Python ctypes的简单使用

1 背景

上星期申请了Prof. Anthoniou的实验室做HiWi,进入Vanpooling项目这个组,负责帮忙实现Enhancement Demand Model (Python)和Scheduler (C++)之间的交互。虽然说是交互,其实就是需要将Demand Model输出的结果作为函数参数通过一定的方式传入Scheduler中进行计算。期间尝试了多种方法:

  • 利用SWIG来将写好的Scheduler翻译成Python的一个module,然后在Python里直接调用。但是由于两种语言在各种数据类型的定义上存在区别,所以有一些参数很难用Python定义的变量带入,比如说指针类型,甚至字符串类型。
  • 利用C++的拓展包Boost.Python来定义Scheduler需要的参数,这样就能够将Python定义的变量直接使用
    (同样需要用SWIG先翻译再导入到Python中)。但是Boost的安装摸了半天还是没能实现一个最起码的Hello, world!程序,所以只好暂时先放放另谋出路,不过以后还是会继续学习这个包的。
  • 利用Python里的ctypesmodule来实现。因为仔细想了想,我们所需要做的像防御只是将Python的数据类型转化为C++所能接受的数据类型传入到Scheduler里进行计算。而ctypes本来就是专门为此而生的,所以这是最简单直接的方法。

从而便花了一点时间去简单地学习了下如何利用ctypes去在Python中定义出可以传给C++编写的程序做实际参数的变量。官方所给的说明文档虽然很好很全面,但是给的例子还是太少,好在还是发现了一篇总结得不错的,给出很多有用例子的博文A。
备注:以下操作所用工具MinGWPython3.6

2 利用ctypes的大概流程

其实ctypes是设计来方便PythonC之间的交互的,而不是用来实现PythonC++数据类型的转化的,但是C++也是有方法将自身用C来进行编译的,只需要简单的利用下面的语句就能够实现。

extern "C" { // 注意一下C好像必须要用大写
...这里是代码...
}
  1. 首先要用命令行工具cd到程序文件所在的文件夹。
  2. 然后,要将程序文件编译为.dll文件
  • 如果用的是C来写的程序,假设程序名称为test.c,则执行:
gcc -shared -fPIC test.c -o test.dll
  • 如果用的是C++来写的程序,假设程序名称为test.cpp,则执行:
g++ -shared -fPIC test.cpp -o test.dll
  1. 最后在Python的程序文件中将其导入即可。
from ctypes import *
test = CDLL('test.dll') #test便是程序的一个实例,可以对C或者C++程序里所定义的函数进行调用

3 一些数据类型的转化

当然,关键的还是如何在Python里能够定义出C++的数据类型以便于将其传入到C++里进行计算。由于我们用到的是C++的程序,所以下面的例子都是关于在Python里实现C++数据类型的实现的。

3.1 官方给出的基础转换表

一些基础数据类型可以直接对照下表利用ctypesPython中定义出相应的C++的数据类型 (C语言和C++的基础数据类型好像是一样的?)。

官方文档中给出的基础数据类型对照表

下面给出一个简单的例子。要先说明一个需要注意的问题是,在Python程序中调用函数时,我们要为手动为函数设定函数的参数类型,如果是有返回值的函数还需要设置返回值的类型,因为按照博文A的说法:

出现此问题的原因在于DLL文件无法在调用其中函数时自动设定数据类型,而如果不对类型进行设定,则调用函数时默认的输入、输出类型均为int。故如果函数的参数或返回值包含非int类型时,就需要对函数的参数以及返回值的数据类型进行设定。

而本人也尝试过,确实是需要的,否则会有错误提示。但是似乎如果是结构体变量,或者指针变量作为输入时则不需要。

test.cpp

#include 
using namespace std;

extern "C" {
    char py2c(int x, double y, char z);
}

char py2c(int x, double y, char z){
    cout << x << " belong to int type!" << endl;
    cout << y << " belong to double type!" << endl;
    cout << z << " belong to char type!" << endl;
    cout << "x + y = " <

test.py

from ctypes import *
test = CDLL('test.dll')
_x = c_int(1)
_y = c_double(1)
_z = c_char(b'z')
test.py2c.argtypes = (c_int, c_double, c_char)
test.py2c.restypes = c_char
test.py2c(_x, _y, _z)

3.2 字符串和字符串数组类型,其它的数据类型的数组以及二维数组

  • 字符串,整型数组,浮点型数组
str1 = (c_char*3)() # 生成一个长度为3,且全部为None的字符串
str2 = (c_char*4)('a', 'b', 'c') #生成一个长度为4,且前三个字符为abc,最后一个为None的字符串
int1 = (c_int*5)()
float1 = (c_double*6)()
  • 字符串数组、二维数组
    其实字符串数组就是靠二维数组来实现的
(c_int*5)*3 # 生成5*3的二维数组
((c_int*6)*5)((1,2,3,4,5), (6,6,6), (7,7,7)) # 生成6*5的数组并进行初始化,其余未初始化的元素为0
((c_char*5)*3)() # 生成一个字符串数组!!!

字符串数组的赋值需要用到create_string_buffer这个函数

strArr = ((c_char*5)*3)()
charList = ['aaa', 'bbb', 'ccc']
for row in range(3):
# create_string_buffer的第一个参数是赋值的内容,第二个参数是数组中每一个字符串的长度
# 而且这个数字必须和赋值的对象长度相同,比如这里根据定义strArr没一个字符串长度都是5,
# 所以create_string_buffer的第二个参数也必须是5
    strArr[row] = create_string_buffer(charList[row].encode(), 5)
for row in strArr:
    print(row.value.decode())

其中的encode是用于将string转为bytes,而decode则相反。

4 类方法的调用

这里完全是将博文A里的例子给搬过来了,因为觉得确实是个不错的方法。像下面这样定义函数就能够直接对hello()函数进行调用了。

extern "C" {
    void hello();
}
class Test{
    public:
        void hello();
}; // 别漏了分号
void Test::hello(){
    printf("Hello!");
}
void hello(){
    Test *testP = new Test;
    testP->hello();
    delete testP;
}

5 指针类型

ctypes里有三个方法来构建一个变量的指针,分别是byref, pointer, POINTER。其中byref,pointer的对象都只能是ctypes数据类型的,但是好像我用byref来取结构体变量的地址的时候会报错,而用pointer则没有报错,不知道为什么。如果使用的是pointer,则在输出它所指地址内包含的值时必须先用contents这个方法。'POINTER'则是用来创建一个地址类型的,而不是用来取址的。

x = pointer(c_int(3))
print(x.contents.value)
y = byref(c_int(4))
intPointer = POINTER(c_int())

6 结构体

这个就直接给例子吧,自己参悟。
test.cpp

#include
#include

struct Location {
    int node; 
};

struct Demand {
    struct Location *pickup;
    struct Location stops[5]; 
    double real_distance;
};

typedef double SPEED;
typedef SPEED **TS;

extern "C"{
    void handle(Demand *demand, TS ts);
}
void handle(Demand *demand, TS ts){
    std::cout<<"Done!"<

test.py

from ctypes import *

class Location(Structure):
    _fields_ = (
        ('id', c_int),
        ('node', c_int)
    )   

class Demand(Structure):
    _fields_ = (
        ('pickup', POINTER(Location)),
        ('stops', Location*5),
        ('real_distance', c_double)
    )
pickup_loc = Location(23)
_stops = (Location*5)()
demands = Demand(pointer(pickup_loc),_stops,10.2)
ts = c_double(45.6)
TSPointer = POINTER(c_double)

test = CDLL('test.dll')
test.handle(pointer(demands), TSPointer(TSPointer(ts)))

上面的例子是我从写的程序里删减部分后拷贝过来的,应该覆盖了大部分的转换需求,但是我也只是现学不久,可能有些地方有些问题。

你可能感兴趣的:(Python ctypes的简单使用)