Python与C扩展的几种方式
2021年4月15日 - 由Bo 0 评论 997 阅读
python的应用面很广,但是python有个被人诟病的问题就是性能不够理想。不过我们却能看到python在类似科学计算相关的领域仍能发挥作用,这就显得比较矛盾。这是因为python发挥了它的胶水一般的特性,python对于将业务实现和将想法实现是比较快捷方便的,在需要效率和性能的地方则是可以用C或C++来实现。最常用的python(即cpython)与c是比较亲和的,像一些常用的库比如numpy是用c写的。python能方便地与c代码结合在一起。
这里分别介绍ctypes,cython,swig,cffi, python/c api。
ctypes
python的一个模块ctypes可以方便地调用c代码。ctypes提供了一些与c兼容的数据类型和方法,可以调用windows下的.dll文件或linux下的.so文件。
假设我们先写一段简单的c代码:
int add_int(int, int);
float add_float(float, float);
int add_int(int num1, int num2){
return num1 + num2;
}
float add_float(float num1, float num2){
return num1 + num2;
}
因为自己目前用mac,就编译为.so文件:
gcc -shared -Wl,-install_name,adder.so -o adder.so -fPIC add.c
此时可以在python中用ctypes来调用这个so文件:
import ctypes
adder = ctypes.CDLL('./adder.so')
result_int = adder.add_int(4, 5)
print(result_int)
a = ctypes.c_float(5.25)
b = ctypes.c_float(2.81)
add_float = adder.add_float
add_float.restype = ctypes.c_float
result_float = add_float(a, b)
print(result_float)
cython
cython是一种编程语言,以类似python的语法(不相同)来编写c扩展并可被python调用。cython可以将python代码或者python与c或者的pyx代码转换为c,以此提升运行效率;也可辅助python调用c代码中的函数。算是一种常用的提升python性能的方式。
先安装cython:
pip install cython
接下来准备3个文件,功能是一样的:
# pure_python.py 不转换
def test(x):
y = 1
for i in range(1, x+1):
y *= i
return y
# cy_python.py 转换
def test(x):
y = 1
for i in range(1, x+1):
y *= i
return y
# pyx_python.pyx 转换
cpdef int test(int x):
cdef int y = 1
cdef int i
for i in range(1, x+1):
y *= i
return y
创建setup.py,并分别指明cy_python.py和pyx_python.pyx:
from distutils.core import setup
from Cython.Build import cythonize
setup(
ext_modules = cythonize("pyx_python.pyx") # 执行一次
# ext_modules = cythonize("cy_python.py") # 执行一次
)
#additional
#如果有import第三方包,需要类似如下的:
from distutils.core import setup
from Cython.Build import cythonize
import numpy
setup(
ext_modules=cythonize("try_numpy.pyx"),
include_dirs=[numpy.get_include()]
)
执行命令分别生成so文件:
python setup.py build_ext --inplace
再新增一个测试文件看看所需时间的对比,可以看出pyx的执行时间非常短:
import cy_python
import pure_python
import pyx_python
import time
n = 100000
start = time.time()
pure_python.test(n)
end = time.time()
print(end - start)
time.sleep(1)
start = time.time()
cy_python.test(n)
end = time.time()
print(end - start)
time.sleep(1)
start = time.time()
pyx_python.test(n)
end = time.time()
print(end - start)
time.sleep(1)
swig
swig可以将c代码转换成多种语言的扩展,虽然过程显得麻烦点。
先在mac上安装swig:
brew install swig
按照官方样例,先增加一个example.c文件:
#include <time.h>
double My_variable = 3.0;
int fact(int n) {
if (n <= 1) return 1;
else return n*fact(n-1);
}
int my_mod(int x, int y) {
return (x%y);
}
char *get_time()
{
time_t ltime;
time(<ime);
return ctime(<ime);
}
再添加一个interface文件 example.i:
/* example.i */
%module example
%{
/* Put header files here or function declarations like below */
extern double My_variable;
extern int fact(int n);
extern int my_mod(int x, int y);
extern char *get_time();
%}
extern double My_variable;
extern int fact(int n);
extern int my_mod(int x, int y);
extern char *get_time();
接着运行一些命令:
swig -python example.i
gcc -c example.c example_wrap.c -I/Users/test/.pyenv/versions/3.6.8/include/python3.6m
ld -shared example.o example_wrap.o -o _example.so
如果在运行这几个命令的时候遇到了提示找不到Python.h的错误,往往是-I后面给的路径不对,比如用了pyenv或者virtualenv,此时查找下Python.h的路径(find ./ -name '*Python.h*'),将对应路径加上即可。
如果遇到了提示“ld: unknown option: -shared”,那么可以换做用下面的方式。
先新增一个setup.py:
from distutils.core import setup, Extension
example_module = Extension('_example', sources=['example_wrap.c', 'example.c'])
setup(name='example', ext_modules=[example_module], py_modules=["example"])
再重新运行命令:
swig -python example.i
python setup.py build_ext --inplace
此时便可调用原c代码中的方法了:
import example
example.fact(5)
example.my_mod(7,3)
example.get_time()
cffi
cffi是python的一个库,可以在python文件中调用c代码,和ctypes类似,但不同的是不需要先编译为so文件,允许直接在python代码中加入c代码的语句。
比如这段代码:
from cffi import FFI
ffi = FFI()
ffi.cdef("""
int add(int a, int b);
int sub(int a, int b);
""")
lib = ffi.verify("""
int add(int a,int b){
return a+b;
}
int sub(int a,int b){
return a-b;
}
""")
print(lib.add(10,2))
print(lib.sub(10,2))
cffi还可以生成扩展模块给其他python脚本用,也可用于调用外部c的函数。
python c api
原生的python c api的方式据说是最被广泛使用的。在c代码中操作python的对象。但需要特定的方式来声明python对象,往往能看到PyObject的字样。比如PyObject为PyListType时,可以用PyListSize()来获取获取个数,类似python中的len(list)。
需要先引入Python.h这个头文件,里面定义了所有需要的类型(Python对象类型的表示)和函数定义(对Python对象的操作)。
现在用c来做一个最简单的输出helloworld,先添加一个c_py_hello.c文件:
#include <Python.h>
// Function 1: A simple 'hello world' function
static PyObject* helloworld(PyObject* self, PyObject* args)
{
printf("Hello World\n");
return Py_None;
}
// Our Module's Function Definition struct
// We require this `NULL` to signal the end of our method
// definition
static PyMethodDef myMethods[] = {
{ "helloworld", helloworld, METH_NOARGS, "Prints Hello World" },
{ NULL, NULL, 0, NULL }
};
// Our Module Definition struct
static struct PyModuleDef myModule = {
PyModuleDef_HEAD_INIT,
"c_py",
"Test Module",
-1,
myMethods
};
// Initializes our module using our above struct
PyMODINIT_FUNC PyInit_c_py(void)
{
return PyModule_Create(&myModule);
}
新建一个setup.py:
from distutils.core import setup, Extension
setup(name = 'c_py', version = '1.0', \
ext_modules = [Extension('c_py', ['c_py_hello.c'])])
运行:
python setup.py install
此时该模块就在site-packages中了,可以直接调用:
>>> import c_py
>>> c_py.helloworld()
Hello World
顺便说一句,网上关于addList的对list内数字做总计的样例在目前python3里是用不了的,需要把PyInt_AsLong改成PyLong_AsLong,并把Py_InitModule3换成其他的。如下所示:
//Python.h has all the required function definitions to manipulate the Python objects
#include <Python.h>
//This is the function that is called from your python code
static PyObject* addList_add(PyObject* self, PyObject* args){
PyObject * listObj;
//The input arguments come as a tuple, we parse the args to get the various variables
//In this case it's only one list variable, which will now be referenced by listObj
if (! PyArg_ParseTuple( args, "O", &listObj))
return NULL;
//length of the list
long length = PyList_Size(listObj);
//iterate over all the elements
long i, sum =0;
for(i = 0; i < length; i++){
//get an element out of the list - the element is also a python objects
PyObject* temp = PyList_GetItem(listObj, i);
//we know that object represents an integer - so convert it into C long
long elem = PyLong_AsLong(temp);
sum += elem;
}
//value returned back to python code - another python object
//build value here converts the C long to a python integer
return Py_BuildValue("i", sum);
}
//This is the docstring that corresponds to our 'add' function.
static char addList_docs[] =
"add( ): add all elements of the list\n";
/* This table contains the relavent info mapping -
<function-name in python module>, <actual-function>,
<type-of-args the function expects>, <docstring associated with the function>
*/
static PyMethodDef addList_funcs[] = {
{"add", (PyCFunction)addList_add, METH_VARARGS, addList_docs},
{NULL, NULL, 0, NULL}
};
// Our Module Definition struct
static struct PyModuleDef myModule = {
PyModuleDef_HEAD_INIT,
"addList",
"Add all items in list",
-1,
addList_funcs
};
// Initializes our module using our above struct
PyMODINIT_FUNC PyInit_addList(void)
{
return PyModule_Create(&myModule);
}
一样的添加setup.py:
from distutils.core import setup, Extension
setup(name='addList', version='1.0', \
ext_modules=[Extension('addList', ['c_py_list.c'])])
运行python setup.py install后便可调用了:
>>> import addList
>>> l = [3, 77, 1, 22]
>>> addList.add(l)
103
参考链接:
https://docs.python.org/zh-cn/3/extending/extending.html
https://book.pythontips.com/en/latest/python_c_extension.html
https://python3-cookbook.readthedocs.io/zh_CN/latest/chapters/p15_c_extensions.html
https://medium.com/mathematicallygifted/cython-use-it-to-speed-up-python-code-with-examples-cd5a90b32fc7