1. توسعه پایتون با C یا C++¶
اگر بدانید چگونه به زبان C برنامهنویسی کنید، افزودن ماژولهای توکار جدید به پایتون کاملاً آسان است. چنین ماژولهای توسعهای <extension modules> میتوانند دو کاری انجام دهند که مستقیماً در پایتون امکانپذیر نیست: میتوانند نوعهای شیء توکار جدید را پیادهسازی کنند و میتوانند توابع کتابخانه C و فراخوانهای سیستمی را فراخوانی کنند.
برای پشتیبانی از توسعهها، API پایتون (رابط برنامهنویسی کاربردی) مجموعهای از توابع، ماکروها و متغیرها را تعریف میکند که دسترسی به بیشتر جنبههای سیستم زمان اجرای پایتون را فراهم میکنند. API پایتون با گنجاندن سرآیند "Python.h" در یک پرونده منبع C ادغام میشود.
کامپایل یک ماژول توسعهای به کاربرد موردنظر آن و همچنین به پیکربندی سیستم شما بستگی دارد؛ جزئیات در فصلهای بعدی ارائه شده است.
توجه
رابط توسعه C مختص سیپایتون است و ماژولهای توسعهای روی پیادهسازیهای دیگر پایتون کار نمیکنند. در بسیاری از موارد میتوان از نوشتن توسعههای C پرهیز کرد و قابلیت حمل به پیادهسازیهای دیگر را حفظ نمود. برای مثال، اگر مورد استفادهی شما فراخوانی توابع کتابخانه C یا فراخوانیهای سیستمی است، باید به جای نوشتن کد C سفارشی، استفاده از ماژول ctypes یا کتابخانهی cffi را در نظر بگیرید. این ماژولها به شما اجازه میدهند برای برقراری رابط با کد C، کد پایتون بنویسید و نسبت به نوشتن و کامپایل کردن یک ماژول توسعهای C، بین پیادهسازیهای پایتون قابلیت حمل بیشتری دارند.
1.1. یک مثال ساده¶
بیایید یک ماژول توسعهای به نام spam بسازیم (غذای مورد علاقهی طرفداران مونتی پایتون...) و فرض کنیم میخواهیم یک رابط پایتونی برای تابع کتابخانهی C یعنی system() [1] بسازیم. این تابع یک رشتهی نویسهای پایانیافته با نویسهی تهی را بهعنوان آرگومان میگیرد و یک عدد صحیح برمیگرداند. میخواهیم این تابع از پایتون به صورت زیر فراخوانیپذیر باشد:
>>> import spam
>>> status = spam.system("ls -l")
کار را با ایجاد یک پروندهی spammodule.c آغاز کنید. (از نظر تاریخی، اگر ماژولی spam نام داشته باشد، پروندهی C حاوی پیادهسازی آن spammodule.c نامیده میشود؛ اگر نام ماژول بسیار طولانی باشد، مانند spammify، نام ماژول میتواند تنها spammify.c باشد.)
دو سطر نخست پروندهی ما میتواند باشد:
#define PY_SSIZE_T_CLEAN
#include <Python.h>
که API پایتون را وارد میکند (در صورت تمایل میتوانید کامنتی که هدف ماژول را شرح میدهد و یک اعلان حق نشر اضافه کنید).
توجه
از آنجا که پایتون ممکن است برخی تعریفهای پیشپردازنده را تعریف کند که در برخی سیستمها بر سرآیندهای استاندارد تأثیر میگذارند، شما باید Python.h را پیش از گنجاندن هر سرآیند استانداردی بگنجانید.
#define PY_SSIZE_T_CLEAN برای نشان دادن اینکه Py_ssize_t باید در برخی APIها بهجای int استفاده شود، به کار میرفت. از پایتون 3.13 به بعد دیگر ضروری نیست، اما ما آن را برای سازگاری با گذشته در اینجا نگه میداریم. برای توضیح این ماکرو به رشتهها و بافرها مراجعه کنید.
تمام نمادهای قابل مشاهده توسط کاربر که توسط Python.h تعریف شدهاند، پیشوند Py یا PY دارند؛ بهجز نمادهایی که در پروندههای سرآیند استاندارد تعریف شدهاند.
نکته
برای سازگاری با نسخههای پیشین، Python.h چندین پروندهی سرآیند استاندارد را include میکند. افزونههای C باید سرآیندهای استاندارد مورد استفادهی خود را include کنند و نباید به این includeهای ضمنی تکیه کنند. اگر از C API محدود نسخهی 3.13 یا جدیدتر استفاده میکنید، includeهای ضمنی عبارتاند از:
<assert.h><intrin.h>(در ویندوز)<inttypes.h><limits.h><math.h><stdarg.h><wchar.h><sys/types.h>(در صورت وجود)
اگر Py_LIMITED_API تعریف نشده باشد، یا روی نسخه 3.12 یا قدیمیتر تنظیم شده باشد، سرآیندهای زیر نیز گنجانده میشوند:
<ctype.h><unistd.h>(در POSIX)
اگر Py_LIMITED_API تعریف نشده باشد، یا روی نسخه 3.10 یا قدیمیتر تنظیم شده باشد، سرآیندهای زیر نیز گنجانده میشوند:
<errno.h><stdio.h><stdlib.h><string.h>
مورد بعدی که به پرونده ماژول خود اضافه میکنیم، تابع C است که هنگام ارزیابی عبارت پایتون spam.system(string) فراخوانی خواهد شد (بهزودی خواهیم دید که چگونه در نهایت فراخوانی میشود):
static PyObject *
spam_system(PyObject *self, PyObject *args)
{
const char *command;
int sts;
if (!PyArg_ParseTuple(args, "s", &command))
return NULL;
sts = system(command);
return PyLong_FromLong(sts);
}
ترجمهای سرراست از فهرست آرگومانها در پایتون (برای مثال، عبارت واحد "ls -l") به آرگومانهای پاسدادهشده به تابع C وجود دارد. تابع C همیشه دو آرگومان دارد که بهطور قراردادی self و args نامیده میشوند.
آرگومان self برای توابع در سطح ماژول به شیء ماژول اشاره میکند؛ برای یک متد، به نمونهی شیء اشاره خواهد کرد.
آرگومان args اشارهگری به یک شیء تاپل پایتون حاوی آرگومانها خواهد بود. هر آیتم از تاپل با یک آرگومان در فهرست آرگومانهای فراخوانی مطابقت دارد. آرگومانها اشیاء پایتون هستند --- برای اینکه بتوانیم در تابع C خود کاری با آنها انجام دهیم، باید آنها را به مقادیر C تبدیل کنیم. تابع PyArg_ParseTuple() در API پایتون انواع آرگومانها را بررسی میکند و آنها را به مقادیر C تبدیل میکند. این تابع از یک رشتهی قالب برای تعیین انواع مورد نیاز آرگومانها و همچنین انواع متغیرهای C که مقادیر تبدیلشده در آنها ذخیره میشوند، استفاده میکند. در ادامه بیشتر دربارهی این موضوع توضیح داده خواهد شد.
تابع PyArg_ParseTuple() در صورتی مقدار درست (غیرصفر) را برمیگرداند که همهی آرگومانها نوع درست را داشته باشند و اجزای آن در متغیرهایی که آدرسهایشان پاس داده شده، ذخیره شده باشند. اگر فهرست آرگومان نامعتبری پاس داده شده باشد، مقدار نادرست (صفر) را برمیگرداند. در حالت دوم، همچنین یک استثنای مناسب ایجاد میکند تا تابع فراخواننده بتواند بلافاصله NULL را برگرداند (همانطور که در مثال دیدیم).
1.2. میانپرده: خطاها و استثناها¶
یک قرارداد مهم در سراسر مفسر پایتون به شرح زیر است: هنگامی که تابعی شکست میخورد، باید وضعیت استثنا را تنظیم کند و یک مقدار خطا (معمولاً -1 یا یک اشارهگر NULL) برگرداند. اطلاعات استثنا در سه عضو از وضعیت نخ مفسر ذخیره میشود. در صورت نبود استثنا، اینها NULL هستند. در غیر این صورت، آنها معادلهای C اعضای تاپل پایتونیای هستند که توسط sys.exc_info() برگردانده میشود. اینها عبارتاند از نوع استثنا، نمونه استثنا، و یک شیء ردگیری. دانستن این موارد برای درک چگونگی انتقال خطاها اهمیت دارد.
API پایتون تعدادی تابع برای تنظیم انواع مختلف استثناها تعریف میکند.
رایجترین آنها PyErr_SetString() است. آرگومانهای آن یک شیء استثنا و یک رشته C هستند. شیء استثنا معمولاً شیء از پیش تعریفشدهای مانند PyExc_ZeroDivisionError است. رشته C علت خطا را نشان میدهد، به یک شیء رشته پایتون تبدیل میشود و به عنوان «مقدار مرتبط» استثنا ذخیره میشود.
تابع مفید دیگر PyErr_SetFromErrno() است که تنها یک آرگومان استثنا میگیرد و مقدار مرتبط را با بررسی متغیر سراسری errno میسازد. عمومیترین تابع PyErr_SetObject() است که دو آرگومان شیء میگیرد: استثنا و مقدار مرتبط با آن. نیازی نیست اشیاء ارسالشده به هیچیک از این توابع را Py_INCREF() کنید.
میتوانید بهصورت غیرمخرب با PyErr_Occurred() بررسی کنید که آیا استثنایی تنظیم شده است یا خیر. این تابع شیء استثنای فعلی را برمیگرداند، یا اگر استثنایی رخ نداده باشد، NULL. شما معمولاً برای فهمیدن اینکه آیا خطایی در فراخوانی یک تابع رخ داده است، نیازی به فراخوانی PyErr_Occurred() ندارید، زیرا باید بتوانید از روی مقدار بازگشتی متوجه آن شوید.
وقتی تابعی f که تابع دیگری به نام g را فراخوانی میکند تشخیص میدهد که آن تابع شکست خورده است، f باید خودش یک مقدار خطا برگرداند (معمولاً NULL یا -1). این تابع نباید یکی از توابع PyErr_* را فراخوانی کند --- یکی از آنها قبلاً توسط g فراخوانی شده است. سپس فراخوانکننده f نیز باید نشانهای از خطا را به فراخوانکننده خودش برگرداند، باز هم بدون فراخوانی PyErr_*، و به همین ترتیب --- دقیقترین علت خطا قبلاً توسط تابعی که نخست آن را تشخیص داده است گزارش شده است. وقتی خطا به حلقه اصلی مفسر پایتون میرسد، این امر کد پایتون در حال اجرا را متوقف میکند و تلاش میکند هندلر استثنایی را که برنامهنویس پایتون تعیین کرده است بیابد.
(گاهی موقعیتهایی پیش میآید که یک ماژول میتواند در واقع با فراخوانی تابع دیگری از نوع PyErr_* پیام خطای دقیقتری ارائه دهد، و در چنین مواردی انجام این کار اشکالی ندارد. با این حال، بهعنوان یک قاعده کلی، این کار ضروری نیست و میتواند باعث از دست رفتن اطلاعاتی درباره علت خطا شود: بیشتر عملیات میتوانند به دلایل مختلفی شکست بخورند.)
برای نادیدهگرفتن استثنایی که توسط یک فراخوانی تابع ناموفق تنظیم شده است، باید وضعیت استثنا بهطور صریح با فراخوانی PyErr_Clear() پاک شود. کد C تنها باید زمانی PyErr_Clear() را فراخوانی کند که نخواهد خطا را به مفسر منتقل کند، بلکه بخواهد خودش بهطور کامل آن را مدیریت کند (احتمالاً با امتحان کردن چیز دیگری، یا وانمود کردن به اینکه هیچ مشکلی پیش نیامده است).
هر فراخوانی ناموفق malloc() باید به یک استثنا تبدیل شود --- فراخوانیکنندهی مستقیم malloc() (یا realloc()) باید PyErr_NoMemory() را فراخوانی کند و خودش نشانگر شکست را برگرداند. همهی توابع ایجادکنندهی شیء (برای مثال، PyLong_FromLong()) از قبل این کار را انجام میدهند، بنابراین این نکته فقط به کسانی مربوط است که malloc() را مستقیماً فراخوانی میکنند.
همچنین توجه داشته باشید که، با استثنای مهم PyArg_ParseTuple() و امثال آن، توابعی که یک وضعیت عدد صحیح برمیگردانند معمولاً مانند فراخوانیهای سیستمی یونیکس، برای موفقیت مقداری مثبت یا صفر و برای شکست -1 برمیگردانند.
در نهایت، هنگامی که نشانگر خطا را برمیگردانید، مراقب باشید که زبالهها را پاکسازی کنید (با انجام فراخوانیهای Py_XDECREF() یا Py_DECREF() برای اشیایی که از قبل ایجاد کردهاید)!
انتخاب اینکه کدام استثنا را برافرازید، کاملاً با شماست. اشیاء C از پیش اعلانشدهای متناظر با تمام استثناهای توکار پایتون وجود دارند، مانند PyExc_ZeroDivisionError، که میتوانید مستقیماً از آنها استفاده کنید. البته، باید استثناها را هوشمندانه انتخاب کنید --- از PyExc_TypeError برای بیان اینکه پروندهای نتوانست باز شود استفاده نکنید (آن مورد احتمالاً باید PyExc_OSError باشد). اگر مشکلی در فهرست آرگومانها وجود داشته باشد، تابع PyArg_ParseTuple() معمولاً استثنای PyExc_TypeError را برمیافرازد. اگر آرگومانی داشته باشید که مقدار آن باید در محدودهی خاصی باشد یا باید شرایط دیگری را برآورده کند، PyExc_ValueError مناسب است.
شما همچنین میتوانید یک استثنای جدید تعریف کنید که مختص ماژول شما باشد. سادهترین راه برای انجام این کار، اعلان یک متغیر شیء سراسری ایستا در ابتدای پرونده است:
static PyObject *SpamError = NULL;
و آن را با فراخوانی PyErr_NewException() در تابع Py_mod_exec ماژول (spam_module_exec()) مقداردهی اولیه کنید:
SpamError = PyErr_NewException("spam.error", NULL, NULL);
از آنجا که SpamError یک متغیر سراسری است، هر بار که ماژول مجدداً مقداردهی اولیه میشود — یعنی هنگامی که تابع Py_mod_exec فراخوانی میشود — بازنویسی خواهد شد.
فعلاً، اجازه دهید از این مسئله اجتناب کنیم: راهاندازی مکرر را با پرتاب ImportError مسدود خواهیم کرد:
static PyObject *SpamError = NULL;
static int
spam_module_exec(PyObject *m)
{
if (SpamError != NULL) {
PyErr_SetString(PyExc_ImportError,
"cannot initialize spam module more than once");
return -1;
}
SpamError = PyErr_NewException("spam.error", NULL, NULL);
if (PyModule_AddObjectRef(m, "SpamError", SpamError) < 0) {
return -1;
}
return 0;
}
static PyModuleDef_Slot spam_module_slots[] = {
{Py_mod_exec, spam_module_exec},
{0, NULL}
};
static struct PyModuleDef spam_module = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "spam",
.m_size = 0, // non-negative
.m_slots = spam_module_slots,
};
PyMODINIT_FUNC
PyInit_spam(void)
{
return PyModuleDef_Init(&spam_module);
}
توجه داشته باشید که نام پایتونی شیء استثنا spam.error است. تابع PyErr_NewException() میتواند کلاسی ایجاد کند که کلاس پایهی آن Exception باشد (مگر اینکه کلاس دیگری به جای NULL ارسال شود)؛ این کلاس پایه در Built-in Exceptions توصیف شده است.
همچنین توجه داشته باشید که متغیر SpamError ارجاعی به کلاس استثنای بهتازگی ایجادشده را نگه میدارد؛ این کار عمدی است! از آنجا که ممکن است کد خارجی استثنا را از ماژول حذف کند، به یک ارجاع مالکانه (owned reference) به کلاس نیاز است تا اطمینان حاصل شود که کلاس دور انداخته نمیشود و در نتیجه SpamError به اشارهگر سرگردان (dangling pointer) تبدیل نمیشود. اگر این اشارهگر سرگردان شود، کد C که استثنا را برمیانگیزد ممکن است باعث برونریزی هسته یا سایر اثرات جانبی ناخواسته شود.
در حال حاضر، فراخوانی Py_DECREF() برای حذف این ارجاع وجود ندارد. حتی هنگام خاموش شدن مفسر پایتون، متغیر سراسری SpamError زبالهروبی نخواهد شد. این متغیر «نشت» خواهد کرد. با این حال، اطمینان حاصل کردیم که این اتفاق حداکثر یک بار به ازای هر فرایند رخ خواهد داد.
استفاده از PyMODINIT_FUNC بهعنوان نوع بازگشتی تابع را در ادامهی این نمونه بحث میکنیم.
میتوانید استثنای spam.error را در ماژول توسعهای خود با فراخوانی PyErr_SetString() ایجاد کنید، همانطور که در ادامه نشان داده شده است:
static PyObject *
spam_system(PyObject *self, PyObject *args)
{
const char *command;
int sts;
if (!PyArg_ParseTuple(args, "s", &command))
return NULL;
sts = system(command);
if (sts < 0) {
PyErr_SetString(SpamError, "System command failed");
return NULL;
}
return PyLong_FromLong(sts);
}
1.3. بازگشت به مثال¶
با بازگشت به تابع مثال خودمان، اکنون باید بتوانید این دستور را درک کنید:
if (!PyArg_ParseTuple(args, "s", &command))
return NULL;
در صورت تشخیص خطا در فهرست آرگومانها، این تابع NULL (نشانگر خطا برای توابعی که اشارهگر شیء برمیگردانند) را برمیگرداند و به استثنای تنظیمشده توسط PyArg_ParseTuple() تکیه میکند. در غیر این صورت، مقدار رشتهی آرگومان به متغیر محلی command کپی شده است. این یک انتساب اشارهگر است و شما نباید رشتهای را که به آن اشاره میکند تغییر دهید (بنابراین در C استاندارد، متغیر command باید بهدرستی بهصورت const char *command اعلان شود).
دستور بعدی، تابع یونیکسی system() را فراخوانی میکند و رشتهای را که بهتازگی از PyArg_ParseTuple() دریافت کردهایم به آن پاس میدهد:
sts = system(command);
تابع spam.system() ما باید مقدار sts را بهعنوان یک شیء پایتون بازگرداند. این کار با استفاده از تابع PyLong_FromLong() انجام میشود.
return PyLong_FromLong(sts);
در این حالت، یک شیء عدد صحیح را برمیگرداند. (بله، در پایتون حتی اعداد صحیح هم اشیایی روی هیپ هستند!)
اگر تابعی در C داشته باشید که هیچ آرگومان مفیدی برنمیگرداند (تابعی که void برمیگرداند)، تابع پایتونِ متناظر باید None را برگرداند. برای انجام این کار به این اصطلاح نیاز دارید (که توسط ماکروی Py_RETURN_NONE پیادهسازی شده است):
Py_INCREF(Py_None);
return Py_None;
Py_None نام C برای شیء ویژهی پایتون None است. این یک شیء واقعی پایتون است، نه یک اشارهگر NULL که همانطور که دیدیم، در بیشتر زمینهها به معنای «خطا» است.
1.4. جدول متدهای ماژول و تابع مقداردهی اولیه¶
قول داده بودم که نشان دهم spam_system() چگونه از برنامههای پایتون فراخوانی میشود. ابتدا، باید نام و نشانی آن را در یک «جدول متد» فهرست کنیم:
static PyMethodDef spam_methods[] = {
...
{"system", spam_system, METH_VARARGS,
"Execute a shell command."},
...
{NULL, NULL, 0, NULL} /* Sentinel */
};
به ورودی سوم (METH_VARARGS) توجه کنید. این یک پرچم است که به مفسر اعلام میکند از کدام قرارداد فراخوانی برای تابع C استفاده شود. این پرچم معمولاً همیشه باید METH_VARARGS یا METH_VARARGS | METH_KEYWORDS باشد؛ مقدار 0 به این معنی است که گونهی منسوخی از PyArg_ParseTuple() به کار میرود.
هنگام استفادهی تنها از METH_VARARGS، تابع باید انتظار داشته باشد که پارامترهای سطح پایتون بهصورت تاپلی مناسب برای پارس کردن توسط PyArg_ParseTuple() ارسال شوند؛ اطلاعات بیشتر دربارهی این تابع در ادامه ارائه شده است.
اگر قرار است آرگومانهای کلیدواژهای به تابع ارسال شوند، میتوان بیت METH_KEYWORDS را در فیلد سوم تنظیم کرد. در این حالت، تابع C باید پارامتر سومی از نوع PyObject * را بپذیرد که دیکشنریای از کلیدواژهها خواهد بود. برای پارس کردن آرگومانهای چنین تابعی، از PyArg_ParseTupleAndKeywords() استفاده کنید.
جدول متدها باید در ساختار تعریف ماژول ارجاع داده شود:
static struct PyModuleDef spam_module = {
...
.m_methods = spam_methods,
...
};
این ساختار، به نوبه خود، باید در تابع مقداردهی اولیهی ماژول، به مفسر پاس داده شود. تابع مقداردهی اولیه باید نام PyInit_name() داشته باشد، که در آن name نام ماژول است، و باید تنها آیتم غیر static تعریفشده در پروندهی ماژول باشد:
PyMODINIT_FUNC
PyInit_spam(void)
{
return PyModuleDef_Init(&spam_module);
}
توجه داشته باشید که PyMODINIT_FUNC تابع را با نوع بازگشتی PyObject * اعلام میکند، هرگونه اعلان پیوند (linkage) خاص مورد نیاز پلتفرم را اعلام میکند و برای C++ تابع را بهصورت extern "C" اعلام میکند.
PyInit_spam() هنگامی فراخوانی میشود که هر مفسر، ماژول spam خود را برای نخستین بار ایمپورت کند. (برای نکات مربوط به تعبیه پایتون، به ادامه مراجعه کنید.) اشارهگری به تعریف ماژول باید از طریق PyModuleDef_Init() بازگردانده شود تا سازوکار ایمپورت بتواند ماژول را ایجاد کرده و آن را در sys.modules ذخیره کند.
هنگام تعبیه پایتون، تابع PyInit_spam() بهطور خودکار فراخوانی نمیشود، مگر آنکه ورودیای در جدول PyImport_Inittab وجود داشته باشد. برای افزودن ماژول به جدول مقداردهی اولیه، از PyImport_AppendInittab() استفاده کنید و در صورت تمایل، ماژول را ایمپورت کنید:
#define PY_SSIZE_T_CLEAN
#include <Python.h>
int
main(int argc, char *argv[])
{
PyStatus status;
PyConfig config;
PyConfig_InitPythonConfig(&config);
/* Add a built-in module, before Py_Initialize */
if (PyImport_AppendInittab("spam", PyInit_spam) == -1) {
fprintf(stderr, "Error: could not extend in-built modules table\n");
exit(1);
}
/* Pass argv[0] to the Python interpreter */
status = PyConfig_SetBytesString(&config, &config.program_name, argv[0]);
if (PyStatus_Exception(status)) {
goto exception;
}
/* Initialize the Python interpreter. Required.
If this step fails, it will be a fatal error. */
status = Py_InitializeFromConfig(&config);
if (PyStatus_Exception(status)) {
goto exception;
}
PyConfig_Clear(&config);
/* Optionally import the module; alternatively,
import can be deferred until the embedded script
imports it. */
PyObject *pmodule = PyImport_ImportModule("spam");
if (!pmodule) {
PyErr_Print();
fprintf(stderr, "Error: could not import module 'spam'\n");
}
// ... use Python C API here ...
return 0;
exception:
PyConfig_Clear(&config);
Py_ExitStatusException(status);
}
توجه
اگر یک متغیر سراسری یا یک متغیر ایستای محلی تعریف کنید، ماژول ممکن است هنگام مقداردهی مجدد دچار عوارض جانبی ناخواسته شود، برای مثال هنگام حذف ورودیها از sys.modules یا ایمپورت کردن ماژولهای کامپایلشده در چندین مفسر درون یک فرایند (یا پس از یک fork() بدون exec() در میان). اگر وضعیت ماژول هنوز بهطور کامل جداسازیشده نباشد، نویسندگان باید در نظر بگیرند که ماژول را بهعنوان فاقد پشتیبانی از زیرمفسرها علامتگذاری کنند (از طریق Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED).
یک ماژول نمونهی جامعتر در توزیع کد منبع پایتون بهصورت Modules/xxlimited.c گنجانده شده است. میتوان از این پرونده بهعنوان یک قالب استفاده کرد یا صرفاً آن را بهعنوان مثال خواند.
1.5. کامپایل و پیونددهی (linkage)¶
پیش از آنکه بتوانید از ماژول توسعهای جدید خود استفاده کنید، باید دو کار دیگر انجام دهید: کامپایل کردن و پیوند دادن آن با سیستم پایتون. اگر از بارگذاری پویا استفاده کنید، جزئیات ممکن است به سبک بارگذاری پویایی که سیستم شما از آن استفاده میکند بستگی داشته باشد؛ برای اطلاعات بیشتر دربارهی این موضوع، به فصلهای مربوط به ساخت ماژولهای توسعهای (فصل ساخت توسعههای C و C++) و اطلاعات تکمیلیای که تنها به ساخت روی ویندوز مربوط میشود (فصل ساخت ماژولهای توسعهای C و ++C در ویندوز) مراجعه کنید.
اگر نتوانید از بارگذاری پویا استفاده کنید، یا اگر میخواهید ماژول خود را به بخشی دائمی از مفسر پایتون تبدیل کنید، باید تنظیمات پیکربندی را تغییر دهید و مفسر را بازسازی کنید. خوشبختانه، این کار در یونیکس بسیار ساده است: کافی است پروندهی خود (برای مثال spammodule.c) را در پوشهی Modules/ یک توزیع کد منبعِ استخراجشده قرار دهید، و سطری به پروندهی Modules/Setup.local اضافه کنید که پروندهی شما را توصیف کند:
spam spammodule.o
و مفسر را با اجرای make در پوشهی سطح بالا بازسازی کنید. همچنین میتوانید make را در زیرپوشهی Modules/ اجرا کنید، اما در این صورت باید ابتدا Makefile را در آنجا با اجرای 'make Makefile' بازسازی کنید. (این کار هر بار که پروندهی Setup را تغییر میدهید، ضروری است.)
اگر ماژول شما به کتابخانههای اضافی برای پیوند دادن نیاز دارد، میتوانید آنها را نیز در همان سطر در پرونده پیکربندی فهرست کنید، برای مثال:
spam spammodule.o -lX11
1.6. فراخوانی توابع پایتون از زبان C¶
تا اینجا بر فراخوانیپذیر کردن توابع C از پایتون تمرکز کردهایم. حالت معکوس نیز کاربردی است: فراخوانی توابع پایتون از C. این موضوع بهویژه در مورد کتابخانههایی که از توابعی به اصطلاح «کالبک» پشتیبانی میکنند صادق است. اگر یک رابط C از کالبکها استفاده کند، معادل پایتونی آن اغلب نیاز دارد که سازوکار کالبکی را در اختیار برنامهنویس پایتون قرار دهد؛ پیادهسازی این کار مستلزم فراخوانی توابع کالبک پایتون از یک کالبک C خواهد بود. کاربردهای دیگری نیز قابل تصور است.
خوشبختانه، مفسر پایتون بهراحتی بهصورت بازگشتی فراخوانی میشود و یک رابط استاندارد برای فراخوانی یک تابع پایتون وجود دارد. (اگر میخواهید بدانید چگونه میتوان پارسر پایتون را با یک رشته خاص بهعنوان ورودی فراخوانی کرد، به لایه سطح بسیار بالا مراجعه کنید.)
فراخوانی یک تابع پایتون آسان است. نخست، برنامه پایتون باید به نحوی شیء تابع پایتون را به شما پاس دهد. شما باید تابعی (یا رابط دیگری) برای انجام این کار فراهم کنید. وقتی این تابع فراخوانی شد، اشارهگری به شیء تابع پایتون را (مراقب باشید که آن را Py_INCREF() کنید!) در یک متغیر سراسری --- یا هر جای دیگری که صلاح میدانید --- ذخیره کنید. برای مثال، تابع زیر ممکن است بخشی از تعریف یک ماژول باشد:
static PyObject *my_callback = NULL;
static PyObject *
my_set_callback(PyObject *dummy, PyObject *args)
{
PyObject *result = NULL;
PyObject *temp;
if (PyArg_ParseTuple(args, "O:set_callback", &temp)) {
if (!PyCallable_Check(temp)) {
PyErr_SetString(PyExc_TypeError, "parameter must be callable");
return NULL;
}
Py_XINCREF(temp); /* Add a reference to new callback */
Py_XDECREF(my_callback); /* Dispose of previous callback */
my_callback = temp; /* Remember new callback */
/* Boilerplate to return "None" */
Py_INCREF(Py_None);
result = Py_None;
}
return result;
}
این تابع باید با استفاده از پرچم METH_VARARGS در مفسر ثبت شود؛ این موضوع در بخش جدول متدهای ماژول و تابع مقداردهی اولیه توضیح داده شده است. تابع PyArg_ParseTuple() و آرگومانهای آن در بخش استخراج پارامترها در توابع توسعهای مستندسازی شدهاند.
ماکروهای Py_XINCREF() و Py_XDECREF() شمارش ارجاع یک شیء را افزایش/کاهش میدهند و در صورت وجود اشارهگرهای NULL ایمن هستند (اما توجه داشته باشید که temp در این زمینه NULL نخواهد بود). اطلاعات بیشتر دربارهی آنها در بخش شمارش ارجاع آمده است.
بعداً، وقتی زمان فراخوانی تابع فرا میرسد، شما تابع Cِ PyObject_CallObject() را فراخوانی میکنید. این تابع دو آرگومان دارد که هر دو اشارهگر به اشیاء پایتونی دلخواه هستند: تابع پایتونی و فهرست آرگومانها. فهرست آرگومانها باید همیشه یک شیء تاپل باشد که طول آن برابر با تعداد آرگومانهاست. برای فراخوانی تابع پایتونی بدون هیچ آرگومانی، NULL یا یک تاپل خالی را ارسال کنید؛ برای فراخوانی آن با یک آرگومان، یک تاپل تکنمونه ارسال کنید. Py_BuildValue() زمانی که رشته قالب آن از صفر یا چند کد قالب درون پرانتز تشکیل شده باشد، یک تاپل برمیگرداند. برای مثال:
int arg;
PyObject *arglist;
PyObject *result;
...
arg = 123;
...
/* Time to call the callback */
arglist = Py_BuildValue("(i)", arg);
result = PyObject_CallObject(my_callback, arglist);
Py_DECREF(arglist);
PyObject_CallObject() یک اشارهگر به شیء پایتون برمیگرداند: این همان مقدار بازگشتی تابع پایتون است. PyObject_CallObject() نسبت به آرگومانهای خود، از نظر شمارش ارجاع خنثی (reference-count-neutral) است. در این مثال، یک تاپل جدید برای استفاده بهعنوان فهرست آرگومانها ساخته میشود که بلافاصله پس از فراخوانی PyObject_CallObject()، Py_DECREF() روی آن اعمال میشود.
مقدار بازگشتی PyObject_CallObject() «جدید» است: یا شیئی کاملاً جدید است، یا شیئی موجود است که شمارش ارجاع آن افزایش یافته است. بنابراین، مگر آنکه بخواهید آن را در یک متغیر سراسری ذخیره کنید، باید به نحوی نتیجه را Py_DECREF() کنید، حتی (بهویژه!) اگر علاقهای به مقدار آن نداشته باشید.
با این حال، پیش از انجام این کار، مهم است که بررسی کنید مقدار بازگشتی NULL نیست. اگر چنین باشد، تابع پایتونی با ایجاد یک استثنا خاتمه یافته است. اگر کد C که PyObject_CallObject() را فراخوانی کرده است از پایتون فراخوانی شده باشد، باید اکنون نشان خطایی را به فراخواننده پایتونی خود بازگرداند تا مفسر بتواند ردگیری پشته را چاپ کند، یا کد پایتونی فراخواننده بتواند استثنا را مدیریت کند. اگر این کار ممکن یا مطلوب نباشد، استثنا باید با فراخوانی PyErr_Clear() پاک شود. برای مثال:
if (result == NULL)
return NULL; /* Pass error back */
...use result...
Py_DECREF(result);
بسته به رابط مورد نظر برای تابع کالبک پایتون، ممکن است لازم باشد فهرست آرگومانها را نیز به PyObject_CallObject() ارائه کنید. در برخی موارد، فهرست آرگومانها نیز توسط برنامه پایتون و از طریق همان رابطی که تابع کالبک را مشخص کرده بود، فراهم میشود. در این صورت میتوان آن را ذخیره کرد و به همان شیوهی شیء تابع از آن استفاده کرد. در موارد دیگر، ممکن است لازم باشد یک تاپل جدید ایجاد کنید تا آن را بهعنوان فهرست آرگومانها ارسال کنید. سادهترین راه انجام این کار، فراخوانی Py_BuildValue() است. برای مثال، اگر بخواهید کد رویدادی از نوع عدد صحیح ارسال کنید، ممکن است از کد زیر استفاده کنید:
PyObject *arglist;
...
arglist = Py_BuildValue("(l)", eventcode);
result = PyObject_CallObject(my_callback, arglist);
Py_DECREF(arglist);
if (result == NULL)
return NULL; /* بازگرداندن خطا */
/* اینجا شاید از نتیجه استفاده شود */
Py_DECREF(result);
به قرارگیری Py_DECREF(arglist) بلافاصله پس از فراخوانی و پیش از بررسی خطا توجه کنید! همچنین توجه داشته باشید که بهطور دقیق این کد کامل نیست: ممکن است Py_BuildValue() با کمبود حافظه مواجه شود و این موضوع باید بررسی شود.
شما همچنین میتوانید با استفاده از PyObject_Call() که از آرگومانها و آرگومانهای کلیدواژهای پشتیبانی میکند، تابعی را با آرگومانهای کلیدواژهای فراخوانی کنید. مانند مثال بالا، از Py_BuildValue() برای ساخت دیکشنری استفاده میکنیم.
PyObject *dict;
...
dict = Py_BuildValue("{s:i}", "name", val);
result = PyObject_Call(my_callback, NULL, dict);
Py_DECREF(dict);
if (result == NULL)
return NULL; /* بازگرداندن خطا */
/* اینجا شاید از نتیجه استفاده کنید */
Py_DECREF(result);
1.7. استخراج پارامترها در توابع توسعهای¶
تابع PyArg_ParseTuple() به شرح زیر تعریف شده است:
int PyArg_ParseTuple(PyObject *arg, const char *format, ...);
آرگومان arg باید یک شیء تاپل حاوی فهرست آرگومانی باشد که از پایتون به یک تابع C پاس داده میشود. آرگومان format باید یک رشته قالب باشد که سینتکس آن در تجزیه آرگومانها و ساخت مقادیر در راهنمای مرجع Python/C API توضیح داده شده است. آرگومانهای باقیمانده باید آدرسهای متغیرهایی باشند که نوع آنها توسط رشته قالب تعیین میشود.
توجه داشته باشید که هرچند PyArg_ParseTuple() بررسی میکند که آرگومانهای پایتون نوعهای مورد نیاز را داشته باشند، نمیتواند اعتبار نشانیهای متغیرهای C که به فراخوانی ارسال شدهاند را بررسی کند: اگر در آنجا اشتباهی کنید، کد شما احتمالاً فروپاشی میکند یا دستکم بیتهای تصادفی در حافظه را بازنویسی میکند. پس مراقب باشید!
توجه داشته باشید که هر ارجاع به شیء پایتون که در اختیار فراخواننده قرار میگیرد، یک ارجاع امانتی است؛ شمارش ارجاع آنها را کاهش ندهید!
چند نمونه فراخوانی:
#define PY_SSIZE_T_CLEAN
#include <Python.h>
int ok;
int i, j;
long k, l;
const char *s;
Py_ssize_t size;
ok = PyArg_ParseTuple(args, ""); /* No arguments */
/* Python call: f() */
ok = PyArg_ParseTuple(args, "s", &s); /* A string */
/* Possible Python call: f('whoops!') */
ok = PyArg_ParseTuple(args, "lls", &k, &l, &s); /* دو long و یک رشته */
/* فراخوانی ممکن پایتون: f(1, 2, 'three') */
ok = PyArg_ParseTuple(args, "(ii)s#", &i, &j, &s, &size);
/* یک جفت عدد صحیح و یک رشته، که اندازهی آن نیز برگردانده میشود */
/* فراخوانی ممکن پایتون: f((1, 2), 'three') */
{
const char *file;
const char *mode = "r";
int bufsize = 0;
ok = PyArg_ParseTuple(args, "s|si", &file, &mode, &bufsize);
/* A string, and optionally another string and an integer */
/* Possible Python calls:
f('spam')
f('spam', 'w')
f('spam', 'wb', 100000) */
}
{
int left, top, right, bottom, h, v;
ok = PyArg_ParseTuple(args, "((ii)(ii))(ii)",
&left, &top, &right, &bottom, &h, &v);
/* A rectangle and a point */
/* Possible Python call:
f(((0, 0), (400, 300)), (10, 10)) */
}
{
Py_complex c;
ok = PyArg_ParseTuple(args, "D:myfunction", &c);
/* a complex, also providing a function name for errors */
/* Possible Python call: myfunction(1+2j) */
}
1.8. پارامترهای کلیدواژهای برای توابع توسعهای¶
تابع PyArg_ParseTupleAndKeywords() به صورت زیر اعلان میشود:
int PyArg_ParseTupleAndKeywords(PyObject *arg, PyObject *kwdict,
const char *format, char * const *kwlist, ...);
پارامترهای arg و format با پارامترهای تابع PyArg_ParseTuple() یکسان هستند. پارامتر kwdict دیکشنری کلیدواژههایی است که بهعنوان پارامتر سوم از رانتایم پایتون دریافت میشود. پارامتر kwlist فهرستی از رشتههاست که با NULL پایان مییابد و پارامترها را شناسایی میکند؛ نامها از چپ به راست با اطلاعات نوع گرفتهشده از format تطبیق داده میشوند. در صورت موفقیت، PyArg_ParseTupleAndKeywords() مقدار true را برمیگرداند؛ در غیر این صورت مقدار false را برمیگرداند و استثنای مناسبی ایجاد میکند.
توجه
تاپلهای تودرتو هنگام استفاده از آرگومانهای کلیدواژهای قابل تجزیه نیستند! پارامترهای کلیدواژهای که پاس داده میشوند و در kwlist وجود ندارند، باعث بهوجود آمدن TypeError میشوند.
در اینجا یک ماژول نمونه که از کلیدواژهها استفاده میکند، بر اساس نمونهای از Geoff Philbrick (philbrick@hks.com) آمده است:
#define PY_SSIZE_T_CLEAN
#include <Python.h>
static PyObject *
keywdarg_parrot(PyObject *self, PyObject *args, PyObject *keywds)
{
int voltage;
const char *state = "a stiff";
const char *action = "voom";
const char *type = "Norwegian Blue";
static char *kwlist[] = {"voltage", "state", "action", "type", NULL};
if (!PyArg_ParseTupleAndKeywords(args, keywds, "i|sss", kwlist,
&voltage, &state, &action, &type))
return NULL;
printf("-- This parrot wouldn't %s if you put %i Volts through it.\n",
action, voltage);
printf("-- Lovely plumage, the %s -- It's %s!\n", type, state);
Py_RETURN_NONE;
}
static PyMethodDef keywdarg_methods[] = {
/* The cast of the function is necessary since PyCFunction values
* only take two PyObject* parameters, and keywdarg_parrot() takes
* three.
*/
{"parrot", (PyCFunction)(void(*)(void))keywdarg_parrot, METH_VARARGS | METH_KEYWORDS,
"Print a lovely skit to standard output."},
{NULL, NULL, 0, NULL} /* sentinel */
};
static struct PyModuleDef keywdarg_module = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "keywdarg",
.m_size = 0,
.m_methods = keywdarg_methods,
};
PyMODINIT_FUNC
PyInit_keywdarg(void)
{
return PyModuleDef_Init(&keywdarg_module);
}
1.9. ساخت مقادیر دلخواه¶
این تابع همتای PyArg_ParseTuple() است. این تابع به صورت زیر اعلان میشود:
PyObject *Py_BuildValue(const char *format, ...);
این تابع مجموعهای از واحدهای قالب (format units) را تشخیص میدهد که مشابه واحدهای شناساییشده توسط PyArg_ParseTuple() هستند، اما آرگومانها (که ورودی تابع هستند، نه خروجی آن) نباید اشارهگر باشند، بلکه باید فقط مقدار باشند. این تابع یک شیء جدید پایتون برمیگرداند که برای بازگرداندن از یک تابع C فراخوانیشده از پایتون مناسب است.
یک تفاوت با PyArg_ParseTuple() این است که در حالی که دومی نیازمند است آرگومان اولش یک تاپل باشد (چرا که فهرستهای آرگومان پایتون همیشه به صورت داخلی به شکل تاپل نمایش داده میشوند)، Py_BuildValue() همیشه یک تاپل نمیسازد. این تابع تنها زمانی یک تاپل میسازد که رشته قالببندی آن دو واحد قالببندی یا بیشتر داشته باشد. اگر رشته قالببندی خالی باشد، None برمیگرداند؛ اگر دقیقاً یک واحد قالببندی داشته باشد، هر شیئی را که آن واحد قالببندی توصیف میکند برمیگرداند. برای اینکه آن را وادار کنید یک تاپل با اندازهی ۰ یا ۱ برگرداند، رشته قالببندی را داخل پرانتز قرار دهید.
مثالها (در سمت چپ فراخوانی، در سمت راست مقدار حاصل در پایتون):
Py_BuildValue("") None
Py_BuildValue("i", 123) 123
Py_BuildValue("iii", 123, 456, 789) (123, 456, 789)
Py_BuildValue("s", "hello") 'hello'
Py_BuildValue("y", "hello") b'hello'
Py_BuildValue("ss", "hello", "world") ('hello', 'world')
Py_BuildValue("s#", "hello", 4) 'hell'
Py_BuildValue("y#", "hello", 4) b'hell'
Py_BuildValue("()") ()
Py_BuildValue("(i)", 123) (123,)
Py_BuildValue("(ii)", 123, 456) (123, 456)
Py_BuildValue("(i,i)", 123, 456) (123, 456)
Py_BuildValue("[i,i]", 123, 456) [123, 456]
Py_BuildValue("{s:i,s:i}",
"abc", 123, "def", 456) {'abc': 123, 'def': 456}
Py_BuildValue("((ii)(ii)) (ii)",
1, 2, 3, 4, 5, 6) (((1, 2), (3, 4)), (5, 6))
1.10. شمارش ارجاع¶
در زبانهایی مانند C یا C++، برنامهنویس مسئول تخصیص پویا و آزادسازی حافظه روی هیپ است. در C، این کار با استفاده از توابع malloc() و free() انجام میشود. در C++، عملگرهای new و delete با معنایی اساساً یکسان به کار میروند و بحث زیر را به مورد C محدود میکنیم.
هر بلوک حافظهای که با malloc() تخصیص دادهشده باشد، باید در نهایت از طریق دقیقاً یک فراخوانی free() به استخر حافظهی در دسترس بازگردانده شود. مهم است که free() در زمان مناسب فراخوانی شود. اگر نشانی یک بلوک فراموش شود اما free() برای آن فراخوانی نشود، حافظهای که آن بلوک اشغال کرده است، تا زمانی که برنامه خاتمه یابد، قابل استفادهی مجدد نخواهد بود. این وضعیت نشت حافظه <memory leak> نامیده میشود. از سوی دیگر، اگر برنامهای free() را برای یک بلوک فراخوانی کند و سپس به استفاده از آن بلوک ادامه دهد، با استفادهی مجدد از آن بلوک از طریق یک فراخوانی دیگر malloc() تداخل ایجاد میکند. این کار استفاده از حافظهی آزادشده <using freed memory> نامیده میشود. این همان پیامدهای بد ارجاع به دادههای مقداردهینشده را دارد --- برونریزی هسته، نتایج نادرست، فروپاشیهای مرموز.
علتهای رایج نشت حافظه، مسیرهای غیرمعمول در کد هستند. برای مثال، ممکن است یک تابع بلوکی از حافظه را تخصیص دهد، محاسبهای انجام دهد و سپس بلوک را دوباره آزاد کند. اکنون تغییری در نیازمندیهای تابع ممکن است آزمونی به محاسبه اضافه کند که شرط خطا را تشخیص میدهد و میتواند زودهنگام از تابع بازگردد. آسان است که آزاد کردن بلوک حافظهی تخصیصیافته هنگام این خروج زودهنگام فراموش شود، بهخصوص وقتی که این خروج بعداً به کد اضافه میشود. چنین نشتهایی، پس از ایجاد، اغلب مدت طولانی تشخیص داده نمیشوند: خروج خطا فقط در بخش کوچکی از همهی فراخوانیها رخ میدهد و بیشتر ماشینهای مدرن حافظهی مجازی فراوانی دارند، بنابراین نشت تنها در فرایند طولانیمدتی که مکرراً از تابع نشتکننده استفاده میکند آشکار میشود. از این رو، مهم است که با داشتن قرارداد یا راهبرد کدنویسیای که این نوع خطاها را به حداقل میرساند، از وقوع نشتها جلوگیری کنید.
از آنجا که پایتون بهطور گسترده از malloc() و free() استفاده میکند، به راهبردی نیاز دارد تا از نشت حافظه و همچنین استفاده از حافظهی آزادشده جلوگیری کند. روش انتخابشده شمارش ارجاع <reference counting> نام دارد. اصل کار ساده است: هر شیء شامل یک شمارنده است که وقتی ارجاعی به شیء در جایی ذخیره میشود، افزایش مییابد و وقتی ارجاعی به آن حذف میشود، کاهش مییابد. وقتی شمارنده به صفر برسد، آخرین ارجاع به شیء حذف شده و شیء آزاد میشود.
راهبردی جایگزین، زبالهروبی خودکار <automatic garbage collection> نامیده میشود. (گاهی شمارش ارجاع نیز بهعنوان یک راهبرد زبالهروبی شناخته میشود، از این رو از واژهی «خودکار» برای تمایز میان این دو استفاده میشود.) مزیت بزرگ زبالهروبی خودکار این است که کاربر نیازی به فراخوانی صریح free() ندارد. (مزیت دیگری که ادعا میشود، بهبود سرعت یا مصرف حافظه است --- هرچند این موضوع واقعیتی قطعی نیست.) نقطهضعف این است که برای C، هیچ زبالهروب خودکاری که واقعاً قابل حمل باشد وجود ندارد، در حالی که شمارش ارجاع را میتوان بهصورت قابل حمل پیادهسازی کرد (تا زمانی که توابع malloc() و free() در دسترس باشند --- که استاندارد C آن را تضمین میکند). شاید روزی زبالهروب خودکاری که بهاندازهی کافی قابل حمل باشد، برای C در دسترس قرار گیرد. تا آن زمان، چارهای جز سازگاری با شمارش ارجاع نداریم.
در حالی که پایتون از پیادهسازی سنتی شمارش ارجاع استفاده میکند، همچنین آشکارساز چرخهای ارائه میدهد که برای تشخیص چرخههای ارجاع کار میکند. این امر به برنامهها اجازه میدهد که نگران ایجاد ارجاعهای دایرهای مستقیم یا غیرمستقیم نباشند؛ این ارجاعها نقطهضعف زبالهروبیای هستند که تنها با استفاده از شمارش ارجاع پیادهسازی شده است. چرخههای ارجاع از اشیایی تشکیل شدهاند که ارجاعهایی (احتمالاً غیرمستقیم) به خود دارند، بهطوری که هر شیء در چرخه تعداد ارجاعی غیر از صفر دارد. پیادهسازیهای متداول شمارش ارجاع نمیتوانند حافظهی متعلق به هیچیک از اشیای درون یک چرخه ارجاع، یا اشیایی که از سوی اشیای درون چرخه به آنها ارجاع شده است، را بازیابی کنند، هرچند هیچ ارجاع دیگری به خود چرخه وجود ندارد.
آشکارساز چرخه میتواند چرخههای زباله را تشخیص دهد و آنها را بازپسگیری کند. ماژول gc راهی برای اجرای آشکارساز (تابع collect())، و همچنین رابطهای پیکربندی و امکان غیرفعالسازی آشکارساز در زمان اجرا را فراهم میکند.
1.10.1. شمارش ارجاع در پایتون¶
دو ماکرو وجود دارد، Py_INCREF(x) و Py_DECREF(x)، که افزایش و کاهش شمارش ارجاع را بر عهده دارند. Py_DECREF() همچنین هنگامی که شمارش به صفر برسد، شیء را آزاد میکند. برای انعطافپذیری، این ماکرو free() را مستقیماً فراخوانی نمیکند --- بلکه فراخوانی را از طریق یک اشارهگر تابع در شیء نوع <type object> مربوط به آن شیء انجام میدهد. برای این منظور (و منظورهای دیگر)، هر شیء همچنین حاوی اشارهگری به شیء نوع خود است.
اکنون پرسش بزرگ این است: چه زمانی باید از Py_INCREF(x) و Py_DECREF(x) استفاده کرد؟ اجازه دهید ابتدا چند اصطلاح را معرفی کنیم. هیچکس «مالک» یک شیء نیست؛ اما شما میتوانید مالک یک ارجاع <own a reference> به یک شیء باشید. اکنون شمارش ارجاع یک شیء بهعنوان تعداد ارجاعهای در مالکیت به آن تعریف میشود. مالک یک ارجاع مسئول است که وقتی دیگر به ارجاع نیازی نیست، Py_DECREF() را فراخوانی کند. مالکیت یک ارجاع میتواند منتقل شود. سه راه برای خلاص شدن از یک ارجاع در مالکیت وجود دارد: انتقال آن، ذخیرهی آن، یا فراخوانی Py_DECREF(). فراموش کردن خلاص شدن از یک ارجاع در مالکیت، باعث نشت حافظه میشود.
همچنین میتوان ارجاعی به یک شیء را به امانت گرفت <borrow> [2]. امانتگیرندهی ارجاع نباید Py_DECREF() را فراخوانی کند. امانتگیرنده نباید شیء را بیشتر از مالکی که ارجاع از او امانت گرفتهشده است، نگه دارد. استفاده از ارجاع امانتی پس از آنکه مالک آن را دور انداخته باشد، خطر استفاده از حافظهی آزادشده را در پی دارد و باید بهطور کامل از آن پرهیز کرد [3].
مزیت امانت گرفتن ارجاع نسبت به مالکیت آن این است که لازم نیست در تمام مسیرهای ممکن در کد، دفع ارجاع را مدیریت کنید --- به عبارت دیگر، با ارجاع امانتی، هنگام خروج زودهنگام در معرض خطر نشت قرار نمیگیرید. عیب امانت گرفتن نسبت به مالکیت این است که در برخی موقعیتهای ظریف، ممکن است در کدی که بهظاهر درست است، از یک ارجاع امانتی پس از آنکه مالکی که ارجاع از او امانت گرفته شده، در واقع آن را دفع کرده باشد، استفاده شود.
یک ارجاع امانتی را میتوان با فراخوانی Py_INCREF() به یک ارجاع ملکی تبدیل کرد. این کار تأثیری بر وضعیت مالکی که ارجاع از او امانت گرفتهشده بود نمیگذارد --- بلکه یک ارجاع ملکی جدید ایجاد میکند و مسئولیتهای کامل مالکیت را به همراه دارد (مالک جدید باید ارجاع را بهدرستی دفع کند، همانطور که مالک قبلی نیز باید این کار را انجام دهد).
1.10.2. قواعد مالکیت¶
هرگاه ارجاعی به یک شیء به داخل یا خارج از یک تابع پاس داده شود، اینکه آیا مالکیت به همراه ارجاع منتقل میشود یا خیر، بخشی از مشخصات رابط تابع است.
بیشتر توابعی که ارجاعی به یک شیء را برمیگردانند، مالکیت را همراه با ارجاع منتقل میکنند. بهطور خاص، تمام توابعی که وظیفهشان ایجاد یک شیء جدید است، مانند PyLong_FromLong() و Py_BuildValue()، مالکیت را به گیرنده منتقل میکنند. حتی اگر شیء در واقع جدید نباشد، شما همچنان مالکیت یک ارجاع جدید به آن شیء را دریافت میکنید. برای نمونه، PyLong_FromLong() یک نهانگاه از مقادیر پرکاربرد را نگه میدارد و میتواند ارجاعی به یک آیتم نهانگاهشده را برگرداند.
بسیاری از توابعی که اشیاء را از اشیاء دیگر استخراج میکنند، مالکیت را نیز همراه با ارجاع منتقل میکنند؛ برای مثال PyObject_GetAttrString(). با این حال، تصویر در اینجا کمتر روشن است، زیرا چند روال رایج استثنا هستند: PyTuple_GetItem()، PyList_GetItem()، PyDict_GetItem() و PyDict_GetItemString() همگی ارجاعهایی را برمیگردانند که شما آنها را از تاپل، فهرست یا دیکشنری امانت میگیرید.
تابع PyImport_AddModule() نیز یک ارجاع امانتی برمیگرداند، هرچند ممکن است در واقع شیئی را که برمیگرداند ایجاد کند: این امر ممکن است زیرا یک ارجاع مالکانه به شیء در sys.modules ذخیره شده است.
وقتی یک ارجاع به شیء را به تابع دیگری میدهید، بهطور کلی، تابع ارجاع را بهصورت امانتی از شما میگیرد --- اگر نیاز به ذخیرهسازی آن داشته باشد، از Py_INCREF() استفاده میکند تا مالکی مستقل شود. دقیقاً دو استثنای مهم برای این قاعده وجود دارد: PyTuple_SetItem() و PyList_SetItem(). این توابع مالکیت آیتمی را که به آنها داده میشود در اختیار میگیرند --- حتی اگر شکست بخورند! (توجه کنید که PyDict_SetItem() و توابع مشابه مالکیت را در اختیار نمیگیرند --- آنها «معمولی» هستند.)
وقتی یک تابع C از پایتون فراخوانی میشود، ارجاعهایی به آرگومانهای خود را از فراخوانیکننده به امانت میگیرد. فراخوانیکننده مالک ارجاعی به شیء است، بنابراین طول عمر ارجاع امانتی تا زمان بازگشت تابع تضمینشده است. تنها زمانی که چنین ارجاع امانتیای باید ذخیره یا منتقل شود، باید با فراخوانی Py_INCREF() به یک ارجاع مالکانه (owned reference) تبدیل شود.
ارجاع شیئی که از یک تابع C فراخوانیشده از پایتون بازگردانده میشود، باید یک ارجاع مالکانه (owned reference) باشد --- مالکیت از تابع به فراخوانندهی آن منتقل میشود.
1.10.3. یخ نازک¶
چند موقعیت وجود دارد که استفادهی بهظاهر بیضرر از یک ارجاع امانتی میتواند منجر به مشکلات شود. همهی این موارد به فراخوانیهای ضمنی مفسر مربوطاند که میتوانند باعث شوند مالک یک ارجاع آن را رها کند.
نخستین و مهمترین موردی که باید از آن آگاه باشید، استفاده از Py_DECREF() روی شیئی نامرتبط در حالی است که ارجاعی به یک آیتم فهرست را به امانت گرفتهاید. برای نمونه:
void
bug(PyObject *list)
{
PyObject *item = PyList_GetItem(list, 0);
PyList_SetItem(list, 1, PyLong_FromLong(0L));
PyObject_Print(item, stdout, 0); /* BUG! */
}
این تابع ابتدا یک ارجاع امانتی به list[0] میگیرد، سپس list[1] را با مقدار 0 جایگزین میکند و در نهایت ارجاع امانتی را چاپ میکند. بیضرر به نظر میرسد، نه؟ اما چنین نیست!
بیایید جریان کنترل را تا داخل PyList_SetItem() دنبال کنیم. فهرست مالک ارجاعهایی به همهی آیتمهای خود است، بنابراین وقتی آیتم ۱ جایگزین میشود، فهرست باید از آیتم اصلی ۱ خلاص شود. اکنون فرض کنید آیتم اصلی ۱ نمونهای از یک کلاس تعریفشده توسط کاربر بوده است، و همچنین فرض کنید این کلاس متد __del__() را تعریف کرده است. اگر شمارش ارجاع این نمونهی کلاس برابر با ۱ باشد، خلاص شدن از آن، متد __del__() آن را فراخوانی میکند. بهطور داخلی، PyList_SetItem() Py_DECREF() را روی آیتم جایگزینشده فراخوانی میکند، که این امر موجب فراخوانی تابع tp_dealloc متناظر با آیتم جایگزینشده میشود. در حین تخصیصزدایی، tp_dealloc tp_finalize را فراخوانی میکند که برای نمونههای کلاس به متد __del__() نگاشت شده است (به PEP 442 مراجعه کنید). کل این دنباله بهصورت همگام درون فراخوانی PyList_SetItem() رخ میدهد.
از آنجا که به زبان پایتون نوشته شده است، متد __del__() میتواند کد پایتون دلخواهی را اجرا کند. آیا ممکن است کاری انجام دهد که ارجاع به item در bug() را بیاعتبار کند؟ قطعاً! با فرض اینکه فهرست پاسدادهشده به bug() در دسترس متد __del__() باشد، میتواند دستوری مانند del list[0] اجرا کند، و با فرض اینکه این آخرین ارجاع به آن شیء باشد، حافظهی مرتبط با آن را آزاد میکند و بدین ترتیب item را بیاعتبار میکند.
راهحل، وقتی منشأ مشکل را بدانید، آسان است: شمارش ارجاع را بهطور موقت افزایش دهید. نسخه صحیح تابع چنین است:
void
no_bug(PyObject *list)
{
PyObject *item = PyList_GetItem(list, 0);
Py_INCREF(item);
PyList_SetItem(list, 1, PyLong_FromLong(0L));
PyObject_Print(item, stdout, 0);
Py_DECREF(item);
}
این داستانی واقعی است. نسخهای قدیمیتر از پایتون شامل گونههایی از این باگ بود و کسی مدت زمان قابل توجهی را در دیباگر C صرف کرد تا بفهمد چرا متدهای __del__() او شکست میخوردند...
دومین مورد از مشکلات مربوط به ارجاع امانتی، گونهای است که به نخها مربوط میشود. بهطور معمول، نخهای متعدد در مفسر پایتون نمیتوانند مزاحم کار یکدیگر شوند، زیرا یک قفل سراسری از کل فضای اشیای پایتون محافظت میکند. با این حال، میتوان این قفل را بهطور موقت با استفاده از ماکروی Py_BEGIN_ALLOW_THREADS آزاد کرد و آن را با استفاده از Py_END_ALLOW_THREADS دوباره به دست آورد. این کار پیرامون فراخوانیهای مسدودکنندهی ورودی/خروجی رایج است تا نخهای دیگر بتوانند در حین انتظار برای تکمیل ورودی/خروجی، از پردازنده استفاده کنند. بدیهی است که تابع زیر همان مشکلی را دارد که تابع قبلی داشت:
void
bug(PyObject *list)
{
PyObject *item = PyList_GetItem(list, 0);
Py_BEGIN_ALLOW_THREADS
...some blocking I/O call...
Py_END_ALLOW_THREADS
PyObject_Print(item, stdout, 0); /* BUG! */
}
1.10.4. اشارهگرهای NULL¶
بهطور کلی، توابعی که ارجاعهای شیء را بهعنوان آرگومان میگیرند، انتظار ندارند که اشارهگرهای NULL را به آنها پاس دهید، و اگر چنین کنید، برونریزی هسته ایجاد میکنند (یا باعث برونریزیهای هستهی بعدی میشوند). توابعی که ارجاعهای شیء را برمیگردانند، معمولاً تنها برای نشان دادن اینکه استثنایی رخ داده است، NULL برمیگردانند. دلیل آزمایش نکردن NULL بودن آرگومانها این است که توابع اغلب اشیایی را که دریافت میکنند به تابع دیگری پاس میدهند --- اگر هر تابعی NULL را آزمایش میکرد، آزمونهای زائد زیادی صورت میگرفت و کد کندتر اجرا میشد.
بهتر است بررسی NULL تنها در "source:" انجام شود؛ زمانی که اشارهگری که ممکن است NULL باشد دریافت میشود، برای مثال از malloc() یا از تابعی که ممکن است استثنا برافراخواند.
ماکروهای Py_INCREF() و Py_DECREF() اشارهگرهای NULL را بررسی نمیکنند --- اما گونههای آنها، Py_XINCREF() و Py_XDECREF()، این بررسی را انجام میدهند.
ماکروهای بررسی یک نوع شیء خاص (Pytype_Check()) اشارهگرهای NULL را بررسی نمیکنند --- باز هم، کدهای زیادی وجود دارند که برای آزمودن یک شیء در برابر انواع مختلف مورد انتظار، چندین مورد از این ماکروها را پشت سر هم فراخوانی میکنند، و این کار آزمونهای تکراری ایجاد میکرد. هیچ گونهای با بررسی NULL وجود ندارد.
سازوکار فراخوانی تابع C تضمین میکند که فهرست آرگومانهایی که به توابع C ارسال میشود (args در مثالها) هرگز NULL نیست --- در واقع تضمین میکند که همیشه یک تاپل است [4].
این خطایی بسیار جدی است که اشارهگر NULL به کاربر پایتون «گریز» کند.
1.11. نوشتن توسعهها با C++¶
نوشتن ماژولهای توسعهای به زبان C++ ممکن است. برخی محدودیتها اعمال میشوند. اگر برنامه اصلی (مفسر پایتون) توسط کامپایلر C کامپایل و پیوند داده شود، نمیتوان از اشیاء سراسری یا ایستای دارای سازنده استفاده کرد. اگر برنامه اصلی توسط کامپایلر C++ پیوند داده شود، این مشکل وجود ندارد. توابعی که توسط مفسر پایتون فراخوانی خواهند شد (بهویژه توابع مقداردهی اولیه ماژول) باید با استفاده از extern "C" اعلام شوند. لازم نیست پروندههای سرآیند پایتون در extern "C" {...} محصور شوند --- این پروندهها در صورتی که نماد __cplusplus تعریف شده باشد، از قبل به این شکل هستند (همهی کامپایلرهای جدید C++ این نماد را تعریف میکنند).
1.12. ارائهی یک C API برای ماژول توسعهای¶
بسیاری از ماژولهای توسعهای صرفاً توابع و نوعهای جدیدی فراهم میکنند که از پایتون مورد استفاده قرار میگیرند، اما گاهی کد موجود در یک ماژول توسعهای میتواند برای ماژولهای توسعهای دیگر مفید باشد. برای مثال، یک ماژول توسعهای میتواند نوعی به نام «مجموعه» پیادهسازی کند که مانند فهرستهای بدون ترتیب کار میکند. درست همانطور که نوع فهرست استاندارد پایتون دارای یک C API است که به ماژولهای توسعهای اجازه میدهد فهرستها را ایجاد و دستکاری کنند، این نوع مجموعه جدید نیز باید مجموعهای از توابع C برای دستکاری مستقیم از سوی ماژولهای توسعهای دیگر داشته باشد.
در نگاه نخست این کار آسان به نظر میرسد: کافی است توابع را بنویسید (البته بدون اینکه آنها را static اعلان کنید)، یک پروندهی سرآیند مناسب فراهم کنید و API زبان C را مستند کنید. و در واقع اگر همه ماژولهای توسعهای همیشه بهصورت ایستا با مفسر پایتون پیوند داده میشدند، همین روش کار میکرد. اما هنگامی که ماژولها بهصورت کتابخانههای اشتراکی استفاده میشوند، ممکن است نمادهای تعریفشده در یک ماژول برای ماژول دیگری نمایان نباشند. جزئیات نمایان بودن نمادها به سیستمعامل بستگی دارد؛ برخی سیستمها برای مفسر پایتون و همه ماژولهای توسعهای از یک فضای نام سراسری واحد استفاده میکنند (مثلاً ویندوز)، در حالی که برخی دیگر در زمان پیوند ماژول به فهرست صریحی از نمادهای واردشده نیاز دارند (AIX یکی از این نمونههاست)، یا امکان انتخاب میان راهبردهای مختلف را ارائه میدهند (بیشتر سیستمهای یونیکس). و حتی اگر نمادها بهصورت سراسری نمایان باشند، ممکن است ماژولی که میخواهید توابعش را فراخوانی کنید، هنوز بارگذارینشده باشد!
بنابراین، قابلیت انتقال (portability) ایجاب میکند که هیچ فرضی دربارهی نمایانی نمادها صورت نگیرد. این بدان معناست که همهی نمادها در ماژولهای توسعهای، بهجز تابع مقداردهی اولیهی ماژول، باید بهصورت static اعلام شوند تا از تداخل نام با ماژولهای توسعهای دیگر پرهیز شود (همانطور که در بخش جدول متدهای ماژول و تابع مقداردهی اولیه بحث شد). و این بدان معناست که نمادهایی که باید برای دیگر ماژولهای توسعهای دسترسیپذیر باشند، باید به شیوهای متفاوت اکسپورت شوند.
پایتون سازوکار ویژهای برای انتقال اطلاعات در سطح C (اشارهگرها) از یک ماژول توسعهای به ماژول توسعهای دیگر فراهم میکند: کپسولها (Capsules). کپسول یک نوع داده پایتونی است که یک اشارهگر (void*) را ذخیره میکند. ایجاد و دسترسی به کپسولها تنها از طریق API زبان C آنها ممکن است، اما میتوان آنها را مانند هر شیء پایتونی دیگری منتقل کرد. بهطور خاص، میتوان آنها را به نامی در فضای نام یک ماژول توسعهای منتسب کرد. سپس ماژولهای توسعهای دیگر میتوانند این ماژول را ایمپورت کنند، مقدار این نام را بازیابی کنند و سپس اشارهگر را از کپسول بازیابی کنند.
از کپسولها میتوان به روشهای بسیاری برای اکسپورت کردن API زبان C یک ماژول توسعهای استفاده کرد. هر تابع میتواند کپسول مخصوص خود را داشته باشد، یا تمام اشارهگرهای API زبان C میتوانند در آرایهای ذخیره شوند که آدرس آن در یک کپسول منتشر میشود. همچنین میتوان وظایف گوناگون ذخیره و بازیابی اشارهگرها را به شیوههای مختلف بین ماژولی که کد را فراهم میکند و ماژولهای مشتری توزیع کرد.
هر روشی را که انتخاب کنید، مهم است که کپسولهای خود را بهدرستی نامگذاری کنید. تابع PyCapsule_New() یک پارامتر نام میگیرد (const char*)؛ شما مجازید نام NULL ارسال کنید، اما ما بهشدت توصیه میکنیم که نامی مشخص کنید. کپسولهای بهدرستی نامگذاریشده درجهای از ایمنی نوع در زمان اجرا فراهم میکنند؛ هیچ راه عملیای برای تمایز یک کپسول بدون نام از کپسول دیگر وجود ندارد.
بهطور خاص، کپسولهایی که برای افشای APIهای C استفاده میشوند، باید مطابق این قرارداد نامگذاری شوند:
modulename.attributename
تابع کمکی PyCapsule_Import() بارگذاری یک C API که از طریق یک کپسول ارائه شده را آسان میکند، اما تنها در صورتی که نام کپسول با این قرارداد مطابقت داشته باشد. این رفتار به کاربران C API درجه بالایی از اطمینان میدهد که کپسولی که بارگذاری میکنند، C API صحیح را در بر دارد.
مثال زیر رویکردی را نشان میدهد که بیشتر بار را بر دوش نویسنده ماژول اکسپورتکننده قرار میدهد، که برای ماژولهای کتابخانهای پرکاربرد مناسب است. این رویکرد تمام اشارهگرهای C API (در مثال فقط یکی!) را در آرایهای از اشارهگرهای void ذخیره میکند که مقدار یک کپسول میشود. پروندهی سرآیند متناظر با ماژول، ماکرویی را فراهم میکند که کار ایمپورت کردن ماژول و بازیابی اشارهگرهای C API آن را انجام میدهد؛ ماژولهای کلاینت تنها باید پیش از دسترسی به C API این ماکرو را فراخوانی کنند.
ماژول اکسپورتکننده، نسخهی تغییریافتهای از ماژول spam در بخش یک مثال ساده است. تابع spam.system() مستقیماً تابع system() از کتابخانهی C را فراخوانی نمیکند، بلکه تابع PySpam_System() را فراخوانی میکند که البته در واقعیت کاری پیچیدهتر انجام میدهد (مانند افزودن "spam" به هر دستور). این تابع PySpam_System() همچنین به ماژولهای توسعهای دیگر اکسپورت میشود.
تابع PySpam_System() یک تابع C ساده است که مانند بقیه موارد بهصورت static اعلان شده است:
static int
PySpam_System(const char *command)
{
return system(command);
}
تابع spam_system() به شکلی ساده تغییر داده میشود:
static PyObject *
spam_system(PyObject *self, PyObject *args)
{
const char *command;
int sts;
if (!PyArg_ParseTuple(args, "s", &command))
return NULL;
sts = PySpam_System(command);
return PyLong_FromLong(sts);
}
در ابتدای ماژول، درست پس از سطر
#include <Python.h>
دو سطر دیگر باید افزوده شود:
#define SPAM_MODULE
#include "spammodule.h"
از #define استفاده میشود تا به پروندهی سرآیند بگوید که در ماژول اکسپورتکننده گنجانده میشود، نه در یک ماژول کلاینت. در نهایت، تابع mod_exec ماژول باید مقداردهی اولیهی آرایه اشارهگرهای C API را بر عهده بگیرد:
static int
spam_module_exec(PyObject *m)
{
static void *PySpam_API[PySpam_API_pointers];
PyObject *c_api_object;
/* Initialize the C API pointer array */
PySpam_API[PySpam_System_NUM] = (void *)PySpam_System;
/* Create a Capsule containing the API pointer array's address */
c_api_object = PyCapsule_New((void *)PySpam_API, "spam._C_API", NULL);
if (PyModule_Add(m, "_C_API", c_api_object) < 0) {
return -1;
}
return 0;
}
توجه داشته باشید که PySpam_API بهصورت static تعریف شده است؛ در غیر این صورت، آرایه اشارهگرها با خاتمهیافتن PyInit_spam() ناپدید میشد!
بخش عمدهی کار در پروندهی سرآیند spammodule.h قرار دارد که به این شکل است:
#ifndef Py_SPAMMODULE_H
#define Py_SPAMMODULE_H
#ifdef __cplusplus
extern "C" {
#endif
/* Header file for spammodule */
/* C API functions */
#define PySpam_System_NUM 0
#define PySpam_System_RETURN int
#define PySpam_System_PROTO (const char *command)
/* Total number of C API pointers */
#define PySpam_API_pointers 1
#ifdef SPAM_MODULE
/* This section is used when compiling spammodule.c */
static PySpam_System_RETURN PySpam_System PySpam_System_PROTO;
#else
/* This section is used in modules that use spammodule's API */
static void **PySpam_API;
#define PySpam_System \
(*(PySpam_System_RETURN (*)PySpam_System_PROTO) PySpam_API[PySpam_System_NUM])
/* Return -1 on error, 0 on success.
* PyCapsule_Import will set an exception if there's an error.
*/
static int
import_spam(void)
{
PySpam_API = (void **)PyCapsule_Import("spam._C_API", 0);
return (PySpam_API != NULL) ? 0 : -1;
}
#endif
#ifdef __cplusplus
}
#endif
#endif /* !defined(Py_SPAMMODULE_H) */
تنها کاری که یک ماژول کلاینت باید انجام دهد تا به تابع PySpam_System() دسترسی داشته باشد، این است که تابع (یا بهتر بگوییم، ماکرو) import_spam() را در تابع mod_exec خود فراخوانی کند:
static int
client_module_exec(PyObject *m)
{
if (import_spam() < 0) {
return -1;
}
/* additional initialization can happen here */
return 0;
}
عیب اصلی این رویکرد این است که پروندهی spammodule.h نسبتاً پیچیده است. با این حال، ساختار پایه برای هر تابعی که اکسپورت میشود یکسان است، بنابراین تنها یک بار باید یاد گرفته شود.
Finally it should be mentioned that Capsules offer additional functionality,
which is especially useful for memory allocation and deallocation of the pointer
stored in a Capsule. The details are described in the Python/C API Reference
Manual in the section کپسولها (Capsules) and in the implementation of Capsules (files
Include/pycapsule.h and Objects/capsule.c in the Python source
code distribution).
پانوشتها