شنبه ۲۹ دي ۱۴۰۳
Tut24 آموزش برنامه نویسی و مجله تخصصی فناوری ورود/عضویت

آموزش پایتون - Extension با C در پایتون

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

یک ماژول افزونه پایتون هیچ چیز بیش از یک کتابخانه معمولی C نیست. در سیستم‌های یونیکس، این کتابخانه‌ها معمولاً با پسوند .so (برای شیء به اشتراک گذاشته شده) پایان می‌یابند. در سیستم‌های ویندوز، شما معمولاً با .dll (برای کتابخانه پیوند دینامیکی) روبرو می‌شوید.

پیش‌نیازها برای نوشتن افزونه‌ها

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

  • در سیستم‌های یونیکس، معمولاً نیاز به نصب یک بستهٔ مخصوص توسعه‌دهندگان مانند python2.5-dev دارید.

  • کاربران ویندوز این سربرگ‌ها را به عنوان بخشی از بسته نصبی پایتون باینری دریافت می‌کنند.

علاوه بر این، فرض می‌شود که شما دارای دانش خوبی در زمینهٔ C یا C++ برای نوشتن هر افزونه پایتونی با استفاده از برنامه‌نویسی C هستید.

نگاهی ابتدایی به یک افزونه پایتون

برای نگاه اولیه به یک ماژول افزونه پایتون، شما باید کد خود را به چهار بخش تقسیم کنید −

  • فایل سربرگ Python.h.

  • توابع C که می‌خواهید به عنوان رابط از ماژول خود ارائه دهید.

  • یک جدول که نام‌های توابع شما را به عنوان آن‌ها را توسط توسعه‌دهندگان پایتون مشاهده می‌شوند به توابع C داخل ماژول ارتباط می‌دهد.

  • یک تابع مقدماتی.

فایل سربرگ Python.h

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

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

توابع C

امضای پیاده‌سازی C توابع شما همیشه یکی از سه شکل زیر را دارد −


static PyObject *MyFunction( PyObject *self, PyObject *args );

static PyObject *MyFunctionWithKeywords(PyObject *self,
                                 PyObject *args,
                                 PyObject *kw);

static PyObject *MyFunctionWithNoArgs( PyObject *self );

هر یک از اعلان‌های پیشین یک شیء پایتون برمی‌گرداند. در پایتون مانند C، چیزی به نام تابع void وجود ندارد. اگر نمی‌خواهید توابعتان مقداری برگردانند، معادل C برای مقدار None پایتون را برگردانید. هدرهای پایتون یک ماکرو به نام Py_RETURN_NONE تعریف کرده‌اند که این کار را برای ما انجام می‌دهد.

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

معمولاً توابع C شما با ترکیب نام ماژول و نام تابع پایتونی به یکدیگر نام‌گذاری می‌شوند، همان‌طور که در اینجا نشان داده شده است −


static PyObject *module_func(PyObject *self, PyObject *args) {
   /* Do your stuff here. */
   Py_RETURN_NONE;
}

این یک تابع پایتونی به نام func درون ماژول module است. شما در حال عموماً قرار دادن اشاره‌گرها به توابع C خود در جدول متد برای ماژول هستید که معمولاً بعداً در کد منبع شما قرار می‌گیرد.

جدول نگاشت متد (Method Mapping Table)

این جدول متد یک آرایه ساده از ساختارهای PyMethodDef است. این ساختار به شکل زیر است −


struct PyMethodDef {
   char *ml_name;
   PyCFunction ml_meth;
   int ml_flags;
   char *ml_doc;
};

اینجا توضیحی از اعضای این ساختار آمده است −

  • ml_name − این نام تابع است که وقتی در برنامه‌های پایتون استفاده می‌شود توسط مفسر پایتون ارائه می‌شود.

  • ml_meth − این باید آدرس یک تابع با یکی از امضایی که در بخش قبلی توضیح داده شد، باشد.

  • ml_flags − این به مفسر می‌گوید که ml_meth از کدام یک از سه امضا استفاده می‌کند.

    • این پرچم معمولاً مقدار METH_VARARGS دارد.

    • اگر می‌خواهید به تابع‌تان اجازهٔ دریافت آرگومان‌های کلیدی هم بدهید، می‌توانید این پرچم را با METH_KEYWORDS به صورت پیوسته بیتی OR کنید.

    • این همچنین مقداری از METH_NOARGS را می‌تواند داشته باشد که نشان دهنده این است که هیچ آرگومانی را نمی‌خواهید قبول کنید.

  • ml_doc − این متن مستند (داک استرینگ) تابع است، که می‌تواند NULL باشد اگر نمی‌خواهید یک مستند بنویسید.

این جدول باید با یک نشان‌دهنده پایان‌بندی که شامل مقادیر NULL و 0 برای اعضای مناسب است، خاتمه یابد.

مثال

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


static PyMethodDef module_methods[] = {
   { "func", (PyCFunction)module_func, METH_NOARGS, NULL },
   { NULL, NULL, 0, NULL }
};

تابع مقدماتی (Initialization Function)

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

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

تابع مقدماتی C شما به طور کلی دارای ساختار کلی زیر است −


PyMODINIT_FUNC initModule() {
   Py_InitModule3(func, module_methods, "docstring...");
}

در ادامه، توضیحی از تابع Py_InitModule3 آمده است −

  • func − این تابع است که قرار است صادر شود.

  • module_methods − این نام جدول نگاشت است که در بالا تعریف شده است.

  • docstring − این نظریه‌یی است که می‌خواهید در افزونه‌تان بنویسید.

ترکیب این‌ها به شکل زیر است −


#include <Python.h>

static PyObject *module_func(PyObject *self, PyObject *args) {
   /* Do your stuff here. */
   Py_RETURN_NONE;
}

static PyMethodDef module_methods[] = {
   { "func", (PyCFunction)module_func, METH_NOARGS, NULL },
   { NULL, NULL, 0, NULL }
};

PyMODINIT_FUNC initModule() {
   Py_InitModule3(func, module_methods, "docstring...");
}

مثال

یک مثال ساده که از تمام مفاهیم بالا استفاده می‌کند −


#include <Python.h>

static PyObject* helloworld(PyObject* self) {
   return Py_BuildValue("s", "Hello, Python extensions!!");
}

static char helloworld_docs[] =
   "helloworld( ): Any message you want to put here!!\n";

static PyMethodDef helloworld_funcs[] = {
   {"helloworld", (PyCFunction)helloworld, 
      METH_NOARGS, helloworld_docs},
      {NULL}
};

void inithelloworld(void) {
   Py_InitModule3("helloworld", helloworld_funcs,
                  "Extension module example!");
}

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

ساخت و نصب افزونه‌ها

بسته distutils راهی بسیار آسان برای توزیع ماژول‌های پایتون، همچنین ماژول‌های افزونه، به روش استاندارد فراهم می‌کند. ماژول‌ها به صورت منبع توزیع می‌شوند و با استفاده از اسکریپت نصب به نام معمولاً setup.py ساخته و نصب می‌شوند.

برای ماژول بالا، شما نیاز به آماده‌سازی اسکریپت setup.py زیر دارید −


from distutils.core import setup, Extension
setup(name='helloworld', version='1.0',  \
      ext_modules=[Extension('helloworld', ['hello.c'])])

حالا، از دستور زیر استفاده کنید، که تمام مراحل کامپایل و لینک‌گذاری مورد نیاز را با دستورات و پرچم‌های درست کامپایلر و لینک‌گذار و کپی کردن کتابخانه پویا حاصل را به دایرکتوری مناسب انجام می‌دهد −

$ python setup.py install

در سیستم‌های مبتنی بر یونیکس، احتمالاً باید این دستور را به عنوان روت (کاربر اصلی) اجرا کنید تا اجازهٔ نوشتن در دایرکتوری site-packages داشته باشید. این معمولاً مشکلی در ویندوز نیست.

وارد کردن افزونه‌ها

بعد از نصب افزونه‌تان، می‌توانید آن را در اسکریپت پایتونی خود وارد کنید و به صورت زیر آن را فراخوانی کنید −


#!/usr/bin/python
import helloworld

print helloworld.helloworld()

این عملکرد زیر را تولید می‌کند −


Hello, Python extensions!!

گذراندن پارامترهای تابع

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


static PyObject *module_func(PyObject *self, PyObject *args) {
   /* Parse args and do something interesting here. */
   Py_RETURN_NONE;
}

جدول متد که شامل ورودی برای تابع جدید است به این شکل خواهد بود −


static PyMethodDef module_methods[] = {
   { "func", (PyCFunction)module_func, METH_NOARGS, NULL },
   { "func", module_func, METH_VARARGS, NULL },
   { NULL, NULL, 0, NULL }
};

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

آرگومان اول به PyArg_ParseTuple آرگومان args است. این شیءی است که شما قصد پارس کردن آن را دارید. آرگومان دوم یک رشتهٔ فرمت است که آرگومان‌ها را به عنوان آنها در آن رشتهٔ فرمت نشان می‌دهید. هر آرگومان با یک یا چند کاراکتر در رشتهٔ فرمت نمایش داده می‌شود به صورت زیر.


static PyObject *module_func(PyObject *self, PyObject *args) {
   int i;
   double d;
   char *s;

   if (!PyArg_ParseTuple(args, "ids", &i, &d, &s)) {
      return NULL;
   }
   
   /* Do something interesting here. */
   Py_RETURN_NONE;
}

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


module.func(1, s="three", d=2.0)
module.func(i=1, d=2.0, s="three")
module.func(s="three", d=2.0, i=1)

احتمالاً می‌توانید حتی تغییرات بیشتری برای آن ایجاد کنید.

تابع PyArg_ParseTuple

در ادامه امضای استاندارد تابع PyArg_ParseTuple آمده است −


int PyArg_ParseTuple(PyObject* tuple,char* format,...)

این تابع برای خطاها مقدار 0 و برای موفقیت مقدار غیر از 0 برمی‌گرداند. tuple شیء PyObject* است که دومین آرگومان تابع C بوده است. در این‌جا format یک رشتهٔ C است که آرگومان‌های اجباری و اختیاری را توصیف می‌کند.

در زیر لیستی از کدهای فرمت برای تابع PyArg_ParseTuple آمده است −

کد نوع C معنی
c char یک رشتهٔ پایتونی به طول 1، به یک char C تبدیل می‌شود.
d double یک عدد اعشاری پایتونی به یک double C تبدیل می‌شود.
f float یک عدد اعشاری پایتونی به یک float C تبدیل می‌شود.
i int یک عدد صحیح پایتونی به یک int C تبدیل می‌شود.
l long یک عدد صحیح پایتونی به یک long C تبدیل می‌شود.
L long long یک عدد صحیح پایتونی به یک long long C تبدیل می‌شود.
O PyObject* شیء پایتونی به صورت مرجع به‌اشتراک گذاشته شده غیر NULL برای آرگومان C می‌شود.
s char* یک رشتهٔ پایتونی بدون null در C به یک char* C تبدیل می‌شود.
s# char*+int هر رشتهٔ پایتونی به یک آدرس و طول C تبدیل می‌شود.
t# char*+int یک بافر تکه تکه‌ای فقط خواندنی به یک آدرس و طول C تبدیل می‌شود.
u Py_UNICODE* یک رشتهٔ یونیکد پایتونی بدون null به C تبدیل می‌شود.
u# Py_UNICODE*+int هر رشتهٔ یونیکد پایتونی به یک آدرس و طول C تبدیل می‌شود.
w# char*+int یک بافر قابل خواندن و نوشتن تکه تکه‌ای به یک آدرس و طول C تبدیل می‌شود.
z char* مانند s، همچنین None را قبول می‌کند (char* C را به NULL تنظیم می‌کند).
z# char*+int مانند s#، همچنین None را قبول می‌کند (char* C را به NULL تنظیم می‌کند).
(...) به تعداد ... یک دنباله پایتونی به عنوان یک آرگومان در نظر گرفته می‌شود.
|   آرگومان‌های زیر اختیاری هستند.
:   پایان فرمت، دنباله نام تابع برای پیغام‌های خطا.
;   پایان فرمت، دنباله کامل متن پیغام خطا.

برگرداندن مقادیر

تابع Py_BuildValue رشتهٔ فرمتی شبیه به PyArg_ParseTuple می‌پذیرد. به جای ارسال آدرس‌های مقادیری که در حال ساخت آنها هستید، مقادیر واقعی را ارسال می‌کنید. در زیر مثالی آورده‌شده که نحوه پیاده‌سازی یک تابع جمع را نشان می‌دهد −


static PyObject *foo_add(PyObject *self, PyObject *args) {
   int a;
   int b;

   if (!PyArg_ParseTuple(args, "ii", &a, &b)) {
      return NULL;
   }
   return Py_BuildValue("i", a + b);
}

این نشان می‌دهد که اگر در پایتون پیاده‌سازی شود، چطور به نظر می‌رسد −


def add(a, b):
   return (a + b)

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


static PyObject *foo_add_subtract(PyObject *self, PyObject *args) {
   int a;
   int b;

   if (!PyArg_ParseTuple(args, "ii", &a, &b)) {
      return NULL;
   }
   return Py_BuildValue("ii", a + b, a - b);
}

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


def add_subtract(a, b):
   return (a + b, a - b)

تابع Py_BuildValue

اینجا امضای استاندارد برای تابع Py_BuildValue آمده است −


PyObject* Py_BuildValue(char* format,...)

اینجا format یک رشتهٔ C است که شیء پایتونی را توصیف می‌کند که قرار است ساخته شود. آرگومان‌های زیری از Py_BuildValue نیز مقادیر C هستند که نتیجه از آنها ساخته می‌شود. نتیجه PyObject* یک مرجع جدید است.

جدول زیر فهرستی از رشته‌های کد معمولی را نشان می‌دهد که صفر یا بیشتر از آنها به یک رشتهٔ فرمت پیوست می‌شود.

کد نوع C معنی
c char یک C char به یک رشتهٔ پایتونی با طول ۱ تبدیل می‌شود.
d double یک C double به یک float پایتونی تبدیل می‌شود.
f float یک C float به یک float پایتونی تبدیل می‌شود.
i int یک C int به یک int پایتونی تبدیل می‌شود.
l long یک C long به یک int پایتونی تبدیل می‌شود.
N PyObject* یک شیء پایتونی را ارسال می‌کند و یک مرجع را بدزدید.
O PyObject* یک شیء پایتونی را ارسال می‌کند و به‌صورت معمول INCREF می‌کند.
O& تبدیل+void* تبدیل دلخواه
s char* یک C 0-terminated char* به یک رشتهٔ پایتونی تبدیل می‌شود، یا NULL به None.
s# char*+int یک C char* و طول به یک رشتهٔ پایتونی تبدیل می‌شود، یا NULL به None.
u Py_UNICODE* یک رشتهٔ C-wide و null-terminated به یک رشتهٔ یونیکد پایتونی تبدیل می‌شود، یا NULL به None.
u# Py_UNICODE*+int یک رشتهٔ C-wide و طول به یک رشتهٔ یونیکد پایتونی تبدیل می‌شود، یا NULL به None.
w# char*+int یک بوفر یک پاره‌ای read/write به آدرس C و طول تبدیل می‌شود.
z char* مانند s، همچنین مقدار None را قبول می‌کند (C char* را به NULL تنظیم می‌کند).
z# char*+int مانند s#، همچنین مقدار None را قبول می‌کند (C char* را به NULL تنظیم می‌کند).
(...) برابر ... یک tuple پایتونی از مقادیر C را بسازد.
[...] برابر ... یک لیست پایتونی از مقادیر C را بسازد.
{...} برابر ... یک دیکشنری پایتونی از مقادیر C را بسازد، کلیدها و مقادیر به تنا alternate می‌شوند.

کد {...} دیکشنری‌ها را از یک تعداد زوجی مقدار C ساخته و به تنا هم کلیدها و هم مقادیر را قرار می‌دهد. به عنوان مثال، Py_BuildValue("{issi}", 23، "zig"، "zag"، 42) یک دیکشنری مشابه {23:'zig'،'zag':42} پایتونی برمی‌گرداند.