Python 3 - การเขียนโปรแกรมส่วนขยายด้วย C

โค้ดใด ๆ ที่คุณเขียนโดยใช้ภาษาที่คอมไพล์เช่น C, C ++ หรือ Java สามารถรวมหรือนำเข้าในสคริปต์ Python อื่นได้ รหัสนี้ถือเป็น "ส่วนขยาย"

โมดูลส่วนขยาย Python ไม่มีอะไรมากไปกว่าไลบรารี C ปกติ ในเครื่อง Unix ไลบรารีเหล่านี้มักจะลงท้ายด้วย.so(สำหรับวัตถุที่ใช้ร่วมกัน) ในเครื่อง Windows โดยทั่วไปคุณจะเห็น.dll (สำหรับไลบรารีที่เชื่อมโยงแบบไดนามิก)

ข้อกำหนดเบื้องต้นสำหรับการเขียนส่วนขยาย

ในการเริ่มเขียนนามสกุลคุณจะต้องมีไฟล์ส่วนหัวของ Python

  • ในเครื่อง Unix โดยปกติจะต้องติดตั้งแพ็คเกจเฉพาะสำหรับนักพัฒนาเช่น.

  • ผู้ใช้ Windows จะได้รับส่วนหัวเหล่านี้เป็นส่วนหนึ่งของแพ็กเกจเมื่อใช้โปรแกรมติดตั้งไบนารี Python

นอกจากนี้สมมติว่าคุณมีความรู้เกี่ยวกับ C หรือ C ++ เป็นอย่างดีในการเขียน Python Extension โดยใช้การเขียนโปรแกรม C

ขั้นแรกให้ดูที่ส่วนขยาย Python

สำหรับการดูโมดูลส่วนขยาย Python เป็นครั้งแรกคุณต้องจัดกลุ่มรหัสของคุณเป็นสี่ส่วน -

  • ไฟล์ส่วนหัวPython.h .

  • ฟังก์ชัน C ที่คุณต้องการแสดงเป็นอินเทอร์เฟซจากโมดูลของคุณ

  • ตารางที่แมปชื่อฟังก์ชันของคุณเนื่องจากนักพัฒนา Python มองว่าเป็นฟังก์ชัน C ภายในโมดูลส่วนขยาย

  • ฟังก์ชันการเริ่มต้น

ไฟล์ส่วนหัว Python.h

คุณต้องรวมไฟล์ส่วนหัวPython.hไว้ในไฟล์ต้นฉบับ C ของคุณซึ่งจะช่วยให้คุณสามารถเข้าถึง Python API ภายในที่ใช้เชื่อมต่อโมดูลของคุณเข้ากับตัวแปล

อย่าลืมใส่ Python.h ก่อนส่วนหัวอื่น ๆ ที่คุณอาจต้องการ คุณต้องทำตามรวมด้วยฟังก์ชันที่คุณต้องการเรียกใช้จาก Python

ฟังก์ชั่น C

ลายเซ็นของการใช้งาน C ในฟังก์ชันของคุณจะใช้รูปแบบใดรูปแบบหนึ่งในสามรูปแบบต่อไปนี้เสมอ -

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

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

static PyObject *MyFunctionWithNoArgs( PyObject *self );

การประกาศก่อนหน้าแต่ละรายการส่งคืนวัตถุ Python ไม่มีสิ่งที่เรียกว่าฟังก์ชันโมฆะใน Python เนื่องจากมีอยู่ใน C หากคุณไม่ต้องการให้ฟังก์ชันของคุณส่งคืนค่าให้คืนค่า C ที่เทียบเท่ากับ PythonNoneมูลค่า. ส่วนหัว Python กำหนดมาโคร Py_RETURN_NONE ที่ทำสิ่งนี้ให้เรา

ชื่อของฟังก์ชัน C ของคุณอาจเป็นชื่ออะไรก็ได้ตามที่คุณต้องการเนื่องจากไม่มีให้เห็นนอกโมดูลส่วนขยาย พวกเขาถูกกำหนดให้เป็นฟังก์ชันคงที่

โดยปกติฟังก์ชัน C ของคุณจะถูกตั้งชื่อโดยการรวมโมดูล Python และชื่อฟังก์ชันเข้าด้วยกันดังที่แสดงไว้ที่นี่ -

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

นี้เป็นฟังก์ชั่นที่เรียกว่างูหลามfuncภายในโมดูลโมดูล คุณจะใส่พอยน์เตอร์ให้กับฟังก์ชัน C ของคุณลงในตารางวิธีการสำหรับโมดูลที่มักจะมาถัดไปในซอร์สโค้ดของคุณ

ตารางการแมปวิธีการ

ตารางวิธีนี้เป็นอาร์เรย์ของโครงสร้าง PyMethodDef อย่างง่าย โครงสร้างนั้นมีลักษณะดังนี้ -

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

นี่คือคำอธิบายของสมาชิกของโครงสร้างนี้ -

  • ml_name - นี่คือชื่อของฟังก์ชันตามที่ล่าม Python นำเสนอเมื่อใช้ในโปรแกรม Python

  • ml_meth - นี่คือที่อยู่ของฟังก์ชันที่มีลายเซ็นใด ๆ ที่อธิบายไว้ในส่วนก่อนหน้า

  • ml_flags - สิ่งนี้จะบอกล่ามว่าใช้ ml_meth สามลายเซ็นใด

    • แฟล็กนี้มักมีค่า METH_VARARGS

    • แฟล็กนี้สามารถเป็นแบบบิตหรือด้วย METH_KEYWORDS หากคุณต้องการอนุญาตให้ใช้อาร์กิวเมนต์คำหลักในฟังก์ชันของคุณ

    • นอกจากนี้ยังสามารถมีค่าของ METH_NOARGS ที่ระบุว่าคุณไม่ต้องการยอมรับข้อโต้แย้งใด ๆ

  • ml_doc - นี่คือ docstring สำหรับฟังก์ชันซึ่งอาจเป็น NULL หากคุณไม่รู้สึกอยากเขียน

ตารางนี้ต้องถูกยกเลิกด้วย Sentinel ที่ประกอบด้วยค่า NULL และ 0 สำหรับสมาชิกที่เหมาะสม

ตัวอย่าง

สำหรับฟังก์ชันที่กำหนดไว้ข้างต้นเรามีตารางการแมปวิธีการดังต่อไปนี้ -

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

ฟังก์ชัน Initialization

ส่วนสุดท้ายของโมดูลส่วนขยายของคุณคือฟังก์ชันการเริ่มต้น ฟังก์ชันนี้ถูกเรียกโดย Python interpreter เมื่อโหลดโมดูล จำเป็นต้องมีการตั้งชื่อฟังก์ชันinitModuleโดยที่โมดูลคือชื่อของโมดูล

ฟังก์ชันการเริ่มต้นจำเป็นต้องส่งออกจากไลบรารีที่คุณจะสร้าง ส่วนหัวของ Python กำหนด 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เพื่อสร้างค่า Python บันทึกโค้ดด้านบนในไฟล์ hello.c เราจะดูวิธีการคอมไพล์และติดตั้งโมดูลนี้เพื่อเรียกใช้จากสคริปต์ Python

การสร้างและติดตั้งส่วนขยาย

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

ในระบบที่ใช้ Unix คุณมักจะต้องรันคำสั่งนี้ในฐานะรูทเพื่อให้มีสิทธิ์ในการเขียนลงในไดเร็กทอรี site-package โดยปกติแล้วจะไม่มีปัญหาใน Windows

การนำเข้าส่วนขยาย

เมื่อคุณติดตั้งส่วนขยายของคุณคุณจะสามารถนำเข้าและเรียกส่วนขยายนั้นในสคริปต์ Python ของคุณได้ดังนี้ -

ตัวอย่าง

#!/usr/bin/python3
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 รูปแบบที่นี่คือสตริง C ที่อธิบายอาร์กิวเมนต์ที่จำเป็นและเป็นทางเลือก

นี่คือรายการรหัสรูปแบบสำหรับไฟล์ PyArg_ParseTuple ฟังก์ชัน -

รหัส ประเภท C ความหมาย
ถ่าน สตริง Python ที่มีความยาว 1 กลายเป็น C char
สองเท่า Python float กลายเป็น C double
ลอย Python float กลายเป็น C float
ผม int Python int กลายเป็น C int
ยาว Python int กลายเป็น C ยาว
ยาวนาน Python int กลายเป็น C long
โอ PyObject * รับการอ้างอิงที่ยืมมาแบบไม่เป็นโมฆะสำหรับอาร์กิวเมนต์ Python
s ถ่าน * สตริง Python ที่ไม่มี nulls ฝังลงใน C char *
s # ถ่าน * + int สตริง Python เป็นที่อยู่และความยาว C
เสื้อ # ถ่าน * + int บัฟเฟอร์ส่วนเดียวแบบอ่านอย่างเดียวถึงที่อยู่และความยาว C
ยู Py_UNICODE * Python Unicode ที่ไม่มี nulls ฝังเป็น C
ยู# Py_UNICODE * + int ที่อยู่และความยาวของ Python Unicode C
w # ถ่าน * + int อ่าน / เขียนบัฟเฟอร์ส่วนเดียวไปยังที่อยู่และความยาว C
z ถ่าน * เช่นเดียวกับ s ยอมรับว่าไม่มี (ตั้งค่า C char * เป็น NULL)
z # ถ่าน * + int เช่นเดียวกับ s # ยอมรับว่าไม่มี (ตั้งค่า C char * เป็น NULL)
(... ) ตาม ... ลำดับ Python จะถือว่าเป็นหนึ่งอาร์กิวเมนต์ต่อรายการ
| อาร์กิวเมนต์ต่อไปนี้เป็นทางเลือก
: สิ้นสุดรูปแบบตามด้วยชื่อฟังก์ชันสำหรับข้อความแสดงข้อผิดพลาด
; สิ้นสุดรูปแบบตามด้วยข้อความแสดงข้อผิดพลาดทั้งหมด

การคืนค่า

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

นี่คือสิ่งที่ดูเหมือนว่าถ้าใช้ใน Python -

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

คุณสามารถคืนค่าสองค่าจากฟังก์ชันของคุณได้ดังนี้ สิ่งนี้จะถูกบันทึกโดยใช้รายการใน 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);
}

นี่คือสิ่งที่ดูเหมือนว่าถ้าใช้ใน Python -

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

Py_BuildValueฟังก์ชั่น

นี่คือลายเซ็นมาตรฐานสำหรับ Py_BuildValue ฟังก์ชัน -

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

รูปแบบที่นี่คือสตริง C ที่อธิบายถึงวัตถุ Python ที่จะสร้าง อาร์กิวเมนต์ของPy_BuildValueต่อไปนี้คือค่า C ที่สร้างผลลัพธ์ PyObject *ผลคือการอ้างอิงใหม่

ตารางต่อไปนี้แสดงรายการสตริงรหัสที่ใช้กันทั่วไปซึ่งรวมศูนย์หรือมากกว่าในรูปแบบสตริง

รหัส ประเภท C ความหมาย
ถ่าน AC char กลายเป็นสตริง Python ที่มีความยาว 1
สองเท่า AC double จะกลายเป็น Python float
ลอย ลูกลอย AC กลายเป็นลูกลอย Python
ผม int AC int กลายเป็น Python int
ยาว AC ยาวกลายเป็น Python int
PyObject * ส่งผ่านวัตถุ Python และขโมยข้อมูลอ้างอิง
โอ PyObject * ส่งผ่านวัตถุ Python และเพิ่มขึ้นตามปกติ
O & แปลง + โมฆะ * การแปลงโดยพลการ
s ถ่าน * C 0 สิ้นสุด char * เป็นสตริง Python หรือ NULL ถึง None
s # ถ่าน * + int C char * และความยาวเป็นสตริง Python หรือ NULL ถึง None
ยู Py_UNICODE * C-wide สตริงที่สิ้นสุดด้วย null เป็น Python Unicode หรือ NULL ถึง None
ยู# Py_UNICODE * + int สตริงกว้าง C และความยาวเป็น Python Unicode หรือ NULL ถึง None
w # ถ่าน * + int อ่าน / เขียนบัฟเฟอร์ส่วนเดียวไปยังที่อยู่และความยาว C
z ถ่าน * เช่นเดียวกับ s ยอมรับว่าไม่มี (ตั้งค่า C char * เป็น NULL)
z # ถ่าน * + int เช่นเดียวกับ s # ยอมรับว่าไม่มี (ตั้งค่า C char * เป็น NULL)
(... ) ตาม ... สร้าง Python tuple จากค่า C
[... ] ตาม ... สร้างรายการ Python จากค่า C
{... } ตาม ... สร้างพจนานุกรม Python จากค่า C สลับคีย์และค่า

โค้ด {... } สร้างพจนานุกรมจากค่า C จำนวนคู่สลับกันระหว่างคีย์และค่า ตัวอย่างเช่น Py_BuildValue ("{issi}", 23, "zig", "zag", 42) จะส่งคืนพจนานุกรมเช่น {23: 'zig', 'zag': 42} ของ Python