Python 3 - Lập trình mở rộng với C

Bất kỳ mã nào bạn viết bằng bất kỳ ngôn ngữ biên dịch nào như C, C ++ hoặc Java đều có thể được tích hợp hoặc nhập vào một tập lệnh Python khác. Mã này được coi là "phần mở rộng".

Một mô-đun mở rộng Python không hơn gì một thư viện C bình thường. Trên máy Unix, các thư viện này thường kết thúc bằng.so(đối với đối tượng dùng chung). Trên máy Windows, bạn thường thấy.dll (đối với thư viện liên kết động).

Điều kiện tiên quyết để viết tiện ích mở rộng

Để bắt đầu viết tiện ích mở rộng của bạn, bạn sẽ cần các tệp tiêu đề Python.

  • Trên máy Unix, điều này thường yêu cầu cài đặt gói dành riêng cho nhà phát triển chẳng hạn như.

  • Người dùng Windows nhận được các tiêu đề này như một phần của gói khi họ sử dụng trình cài đặt Python nhị phân.

Ngoài ra, giả sử rằng bạn có kiến ​​thức tốt về C hoặc C ++ để viết bất kỳ Phần mở rộng Python nào bằng lập trình C.

Đầu tiên hãy xem một Tiện ích mở rộng Python

Để có cái nhìn đầu tiên về mô-đun mở rộng Python, bạn cần phải nhóm mã của mình thành bốn phần:

  • Tệp tiêu đề Python.h .

  • Các chức năng C bạn muốn hiển thị dưới dạng giao diện từ mô-đun của bạn.

  • Một bảng ánh xạ tên các hàm của bạn khi các nhà phát triển Python coi chúng là các hàm C bên trong mô-đun mở rộng.

  • Một chức năng khởi tạo.

Tệp tiêu đề Python.h

Bạn cần bao gồm tệp tiêu đề Python.h trong tệp nguồn C của mình, tệp này cung cấp cho bạn quyền truy cập vào API Python nội bộ được sử dụng để kết nối mô-đun của bạn với trình thông dịch.

Đảm bảo bao gồm Python.h trước bất kỳ tiêu đề nào khác mà bạn có thể cần. Bạn cần tuân theo các hàm mà bạn muốn gọi từ Python.

Các hàm C

Chữ ký của việc triển khai C các chức năng của bạn luôn có một trong ba dạng sau:

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

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

static PyObject *MyFunctionWithNoArgs( PyObject *self );

Mỗi một trong các khai báo trước trả về một đối tượng Python. Không có cái gọi là hàm void trong Python như trong C. Nếu bạn không muốn các hàm của mình trả về giá trị, hãy trả về C tương đương của PythonNonegiá trị. Các tiêu đề Python xác định một macro, Py_RETURN_NONE, thực hiện điều này cho chúng ta.

Tên của các hàm C của bạn có thể là bất cứ thứ gì bạn thích vì chúng không bao giờ được nhìn thấy bên ngoài mô-đun mở rộng. Chúng được định nghĩa là hàm tĩnh .

Các hàm C của bạn thường được đặt tên bằng cách kết hợp mô-đun Python và tên hàm với nhau, như được hiển thị ở đây:

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

Đây là một hàm Python được gọi là func bên trong mô -đun mô-đun . Bạn sẽ đặt các con trỏ đến các hàm C của mình vào bảng phương pháp cho mô-đun thường xuất hiện tiếp theo trong mã nguồn của bạn.

Bảng ánh xạ phương pháp

Bảng phương thức này là một mảng cấu trúc PyMethodDef đơn giản. Cấu trúc đó trông giống như thế này -

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

Đây là mô tả về các thành viên của cấu trúc này -

  • ml_name - Đây là tên của hàm như trình thông dịch Python trình bày khi nó được sử dụng trong các chương trình Python.

  • ml_meth - Đây là địa chỉ của một hàm có bất kỳ một trong các chữ ký được mô tả trong phần trước.

  • ml_flags - Điều này cho trình thông dịch biết mà ml_meth đang sử dụng chữ ký nào.

    • Cờ này thường có giá trị là METH_VARARGS.

    • Cờ này có thể được đặt theo từng bit HOẶC bằng METH_KEYWORDS nếu bạn muốn cho phép các đối số từ khóa vào hàm của mình.

    • Điều này cũng có thể có giá trị METH_NOARGS cho biết bạn không muốn chấp nhận bất kỳ đối số nào.

  • ml_doc - Đây là chuỗi tài liệu cho hàm, có thể là NULL nếu bạn không muốn viết hàm này.

Bảng này cần được kết thúc bằng một sentinel bao gồm các giá trị NULL và 0 cho các thành viên thích hợp.

Thí dụ

Đối với hàm được xác định ở trên, chúng ta có bảng ánh xạ phương thức sau:

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

Hàm khởi tạo

Phần cuối cùng của mô-đun mở rộng của bạn là chức năng khởi tạo. Hàm này được trình thông dịch Python gọi khi mô-đun được tải. Yêu cầu hàm phải được đặt têninitModule, trong đó Mô-đun là tên của mô-đun.

Hàm khởi tạo cần được xuất từ ​​thư viện mà bạn sẽ xây dựng. Các tiêu đề Python xác định PyMODINIT_FUNC để bao gồm các câu chú thích hợp để điều đó xảy ra cho môi trường cụ thể mà chúng ta đang biên dịch. Tất cả những gì bạn phải làm là sử dụng nó khi xác định hàm.

Hàm khởi tạo C của bạn thường có cấu trúc tổng thể sau:

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

Đây là mô tả của Py_InitModule3 chức năng -

  • func - Đây là chức năng được xuất.

  • module_methods - Đây là tên bảng ánh xạ đã xác định ở trên.

  • docstring - Đây là nhận xét bạn muốn đưa ra trong phần mở rộng của mình.

Kết hợp tất cả những điều này lại với nhau, nó trông giống như sau:

#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...");
}

Thí dụ

Một ví dụ đơn giản sử dụng tất cả các khái niệm trên -

#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!");
}

Ở đây, hàm Py_BuildValue được sử dụng để xây dựng một giá trị Python. Lưu mã trên trong tệp hello.c. Chúng ta sẽ xem cách biên dịch và cài đặt mô-đun này để được gọi từ tập lệnh Python.

Xây dựng và Cài đặt Tiện ích mở rộng

Các distutils gói làm cho nó rất dễ dàng để phân phối module Python, cả hai tinh khiết Python và mở rộng mô-đun, trong một cách tiêu chuẩn. Các mô-đun được phân phối ở dạng nguồn, được xây dựng và cài đặt thông qua tập lệnh thiết lập thường được gọi là setup.py as.

Đối với mô-đun trên, bạn cần chuẩn bị tập lệnh setup.py sau:

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

Bây giờ, hãy sử dụng lệnh sau, lệnh này sẽ thực hiện tất cả các bước biên dịch và liên kết cần thiết, với các lệnh và cờ của trình biên dịch và trình liên kết phù hợp, và sao chép thư viện động kết quả vào một thư mục thích hợp -

$ python setup.py install

Trên các hệ thống dựa trên Unix, rất có thể bạn sẽ cần chạy lệnh này với tư cách là người chủ để có quyền ghi vào thư mục gói trang. Đây thường không phải là vấn đề trên Windows.

Nhập tiện ích mở rộng

Sau khi cài đặt tiện ích mở rộng của mình, bạn có thể nhập và gọi tiện ích mở rộng đó trong tập lệnh Python của mình như sau:

Thí dụ

#!/usr/bin/python3
import helloworld

print helloworld.helloworld()

Đầu ra

Điều này sẽ tạo ra kết quả sau:

Hello, Python extensions!!

Thông số chức năng truyền

Vì rất có thể bạn sẽ muốn xác định các hàm chấp nhận đối số, bạn có thể sử dụng một trong các chữ ký khác cho các hàm C của mình. Ví dụ: hàm sau, chấp nhận một số tham số, sẽ được định nghĩa như thế này:

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

Bảng phương thức chứa một mục nhập cho hàm mới sẽ trông như thế này:

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

Bạn có thể sử dụng hàm API PyArg_ParseTuple để trích xuất các đối số từ một con trỏ PyObject được truyền vào hàm C của bạn.

Đối số đầu tiên của PyArg_ParseTuple là đối số args. Đây là đối tượng bạn sẽ phân tích cú pháp . Đối số thứ hai là một chuỗi định dạng mô tả các đối số như bạn mong đợi chúng xuất hiện. Mỗi đối số được biểu diễn bằng một hoặc nhiều ký tự trong chuỗi định dạng như sau.

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;
}

Đầu ra

Biên dịch phiên bản mới của mô-đun của bạn và nhập nó cho phép bạn gọi hàm mới với bất kỳ số lượng đối số nào thuộc bất kỳ loại nào -

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)

Bạn có thể nghĩ ra nhiều biến thể hơn nữa.

Hàm PyArg_ParseTuple

Đây là chữ ký tiêu chuẩn cho PyArg_ParseTuple chức năng -

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

Hàm này trả về 0 cho các lỗi và một giá trị không bằng 0 cho thành công. Tuple là PyObject * là đối số thứ hai của hàm C. Ở đây định dạng là một chuỗi C mô tả các đối số bắt buộc và tùy chọn.

Đây là danh sách các mã định dạng cho PyArg_ParseTuple chức năng -

Loại C Ý nghĩa
c char Một chuỗi Python có độ dài 1 trở thành C char.
d gấp đôi Một float Python trở thành một C double.
f Phao nổi Một float Python trở thành một float C.
Tôi int Một int Python trở thành một int C.
l Dài Một int Python trở thành một C dài.
L dài dài Một int Python trở thành một C dài
O PyObject * Nhận tham chiếu được mượn không phải NULL cho đối số Python.
S char * Chuỗi Python không có null được nhúng vào C char *.
S# char * + int Bất kỳ chuỗi Python nào đến địa chỉ và độ dài C.
t # char * + int Bộ đệm một đoạn chỉ đọc tới địa chỉ và độ dài C.
u Py_UNICODE * Python Unicode không có null được nhúng vào C.
u # Py_UNICODE * + int Bất kỳ địa chỉ và độ dài Python Unicode C nào.
w # char * + int Đọc / ghi bộ đệm một đoạn tới địa chỉ và độ dài C.
z char * Giống như s, cũng chấp nhận None (đặt C char * thành NULL).
z # char * + int Giống như s #, cũng chấp nhận None (đặt C char * thành NULL).
(...) theo ... Một chuỗi Python được coi là một đối số cho mỗi mục.
| Các đối số sau đây là tùy chọn.
: Kết thúc định dạng, theo sau là tên hàm cho các thông báo lỗi.
; Định dạng kết thúc, theo sau là toàn bộ văn bản thông báo lỗi.

Trả lại giá trị

Py_BuildValue lấy một chuỗi định dạng giống như PyArg_ParseTuple . Thay vì chuyển địa chỉ của các giá trị bạn đang xây dựng, bạn chuyển các giá trị thực tế vào. Đây là một ví dụ cho thấy cách triển khai một hàm thêm -

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);
}

Đây là những gì nó sẽ trông như thế nào nếu được triển khai bằng Python -

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

Bạn có thể trả về hai giá trị từ hàm của mình như sau. Điều này sẽ được ghi lại bằng cách sử dụng một danh sách trong Python.

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);
}

Đây là những gì nó sẽ trông như thế nào nếu được triển khai bằng Python -

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

Các Py_BuildValue Chức năng

Đây là chữ ký tiêu chuẩn cho Py_BuildValue chức năng -

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

Ở đây định dạng là một chuỗi C mô tả đối tượng Python để xây dựng. Các đối số sau đây của Py_BuildValue là giá trị C mà từ đó kết quả được tạo. Các PyObject * Kết quả là một tham chiếu mới.

Bảng sau liệt kê các chuỗi mã thường được sử dụng, trong đó không hoặc nhiều hơn được nối thành một định dạng chuỗi.

Loại C Ý nghĩa
c char AC char trở thành một chuỗi Python có độ dài 1.
d gấp đôi AC double trở thành một float trong Python.
f Phao nổi AC float trở thành một float trong Python.
Tôi int AC int trở thành một int Python.
l Dài AC long trở thành một int Python.
N PyObject * Truyền một đối tượng Python và đánh cắp một tham chiếu.
O PyObject * Truyền một đối tượng Python và TĂNG nó như bình thường.
O & chuyển đổi + void * Chuyển đổi tùy ý
S char * C 0 kết thúc char * thành chuỗi Python hoặc NULL thành Không.
S# char * + int Ký tự C * và độ dài thành chuỗi Python hoặc NULL thành Không.
u Py_UNICODE * C-wide, chuỗi kết thúc bằng null thành Python Unicode hoặc NULL thành Không.
u # Py_UNICODE * + int Chuỗi rộng C và độ dài thành Python Unicode hoặc NULL thành Không.
w # char * + int Đọc / ghi bộ đệm một đoạn tới địa chỉ và độ dài C.
z char * Giống như s, cũng chấp nhận None (đặt C char * thành NULL).
z # char * + int Giống như s #, cũng chấp nhận None (đặt C char * thành NULL).
(...) theo ... Tạo bộ tuple Python từ các giá trị C.
[...] theo ... Tạo danh sách Python từ các giá trị C.
{...} theo ... Xây dựng từ điển Python từ các giá trị C, các khóa và giá trị xen kẽ.

Mã {...} xây dựng từ điển từ một số giá trị C chẵn, xen kẽ các khóa và giá trị. Ví dụ: Py_BuildValue ("{Issi}", 23, "zig", "zag", 42) trả về một từ điển như {23: 'zig', 'zag': 42} của Python.