python調用C語言

python因爲在實現多線程的狀況下,因爲GIL(全局解釋器鎖)的存在,只能實現僞線程,要想真正實現多線程,能夠調用第三方的擴展,使用C語言編寫一些須要實現多線程的業務邏輯。

最經常使用的調用C函數的方式,分別是c extension,Cython和ctypes。python

c extension

介紹

python標準庫包含了不少使用C開發的擴展模塊,好比對性能要求很高的json庫。開發者一樣可使用C開發擴展,這是最原始也是最底層的擴展python的方式。linux

示例

demomodule.c

python的擴展模塊由如下幾部分組成:json

  • Python.h
  • C函數
  • 接口函數(python代碼調用的函數)到C函數的映射表
  • 初始化函數
 
// pulls in the Python API 
#include <Python.h>

// C function always has two arguments, conventionally named self and args
// The args argument will be a pointer to a Python tuple object containing the arguments.
// Each item of the tuple corresponds to an argument in the call’s argument list.
static PyObject *
demo_add(PyObject *self, PyObject *args)
{
    const int a, b;
    // convert PyObject to C values
    if (!PyArg_ParseTuple(args, "ii", &a, &b))
        return NULL;
    return Py_BuildValue("i", a+b);
}

// module's method table
static PyMethodDef DemoMethods[] = {
    {"add", demo_add, METH_VARARGS, "Add two integers"},
    {NULL, NULL, 0, NULL} 
};

// module’s initialization function
PyMODINIT_FUNC
initdemo(void)
{
    (void)Py_InitModule("demo", DemoMethods);
}

setup.py

編譯擴展模塊一般使用distutils或setuptools,它會自動調用gcc完成編譯和連接。多線程

from distutils.core import setup, Extension

module1 = Extension('demo',
                    sources = ['demomodule.c']
                    )

setup (name = 'a demo extension module',
       version = '1.0',
       description = 'This is a demo package',
       ext_modules = [module1])

  執行app

python setup.py build_ext --inplace

  會在當前目錄生成一個demo.so。一個python擴展模塊其實就是一個共享庫(.so),它能夠直接在python解釋器中import。函數

--inplace表示將生成的擴展放到源碼所在的目錄,即當前目錄,這樣就能夠直接import而不須要安裝到site-packages目錄。oop

 測試性能

>>> from demo import add
>>> add(1,1)
2
>>> add(1,2)
3
>>> add(1)
Traceback (most recent call last):
  ...
TypeError: function takes exactly 2 arguments (1 given)
>>> add(1,'2')
Traceback (most recent call last):
  ...
TypeError: an integer is required

  

Cython

介紹

Cython聽起來像是一種語言,c與python的結合,這麼說其實沒有錯。python是一種動態類型的解釋型語言,執行效率低,Cython在python的基礎上增長了可選的靜態類型申明的語法,代碼在使用前先被轉換成優化過的C代碼,而後編譯成python擴展庫,大大提高了執行效率。所以從語言的角度來說,Cython是python的超集,即擴展了的python。測試

注意不要和CPython混淆,CPython是用c實現的python解釋器,由官方提供,咱們平時使用的python就是CPython。另外,pypy是python本身實現的python解釋器。Cython是cpython標準庫的一部分,不須要額外安裝。優化

用官網的一句話介紹Cython的做用:

extending the CPython interpreter with fast binary modules, and interfacing Python code with external C libraries.

簡單的說,Cython的兩個主要做用是:

  1. 將python代碼編譯成二進制的擴展模塊,以得到加速;同時能夠在python中使用類型聲明,進一步提高性能;這就意味着可使用python代替c編寫python擴展
  2. 在python代碼裏調用外部的c庫

示例

如今使用Cython從新實現上面的例子——編寫C函數的包裝器。

最終的目錄結構以下

.
├── add_wrapper.c
├── add_wrapper.pyx
├── add_wrapper.so
├── build
│   └── temp.linux-x86_64-2.7
│       └── add_wrapper.o
├── libadd.a
├── libadd.c
├── libadd.h
├── libadd.o
└── setup.py

  

編譯C程序

libadd.h

int add(int a, int b);

  

通常都是經過python調用動態連接庫,須要將生成的庫文件(.so)安裝到標準路徑下(好比/usr/lib)下,連接和運行的時候才能找到該文件,爲了方便這裏以靜態連接庫爲例。

首先將c文件編譯成靜態連接庫:

gcc -c libadd.c
ar rcs libadd.a libadd.o

  第一步會在當前目錄下生成libadd.o,第二步建立靜態連接庫libadd.a

 使用Cython包裝C函數

使用Cython調用c函數很簡單,只須要在Cython中聲明函數的簽名,而後編譯的時候正確地連接外部的動態或靜態庫。

下面就是一個add函數的python包裝器: add_wrapper.pyx

cdef extern from "libadd.h":
    cpdef int add(int a, int b)

  第一行表示引入頭文件libadd.h。第二行聲明該頭文件中的add函數,直接從libadd.h拷貝過來便可,此時只有在Cython模塊內部能調用該C函數,還須要在前面加cpdef聲明,表示暴露出接口給python調用。

 

編譯Cython代碼

Cython是須要編譯成二進制模塊才能使用的,編譯過程包含兩步:

  1. Cython將Cython文件(.pyx)編譯成c代碼(.c)
  2. gcc將c代碼編譯成共享庫(.so)

怎麼編譯呢?最經常使用的方式是編寫一個setup.py文件:

from distutils.core import setup, Extension
from Cython.Build import Cythonize

ext_modules=[
    Extension("add_wrapper",
              sources=["add_wrapper.pyx"],
              extra_objects=['libadd.a']
    )
]

setup(
  name = 'wrapper for libadd',
  ext_modules = Cythonize(ext_modules),
)

  extra_objects表示須要連接的靜態庫文件,也能夠替換成libraries=["add"],library_dirs=["."],鏈接器會自動搜索libadd.solibadd.a,動態連接庫優先。

執行

python setup.py build_ext --inplace

  在當前目錄下會生成add_wrapper.cadd_wrapper.soadd_wrapper.c是第一步編譯生成的中間文件,內容比較長。add_wrapper.so是最終的python二進制模塊,將它放到PYTHONPATH的某個路徑下,就能夠直接import。

若是須要從新build,你可能須要加上--force選項,不然可能不會生效。

測試

>>> from add_wrapper import add
>>> add(1,1)
2
>>> add(2,3)
5
>>> add(-1,1)
0
>>> add(1,False)                                                                                                  
1
>>> add(1)
Traceback (most recent call last):
  ...
TypeError: wrap() takes exactly 2 positional arguments (1 given)
>>>
>>> add(1,'1')
Traceback (most recent call last):
  ...
TypeError: an integer is required

  因而可知,Cython會自動檢查參數類型並完成python對象到C類型的轉換。

ctypes

介紹

ctypes的主要做用就是在python中調用C動態連接庫(shared library)中的函數。

示例

編譯成動態連接庫

libadd.c

int add(int a, int b)
{
    return a + b;
}

  

gcc -shared -o libadd.so libadd.c

  

加載共享庫

使用CDLL動態加載共享庫,一個共享庫對應一個cdll對象。調用cdll的LoadLibrary()方法或直接調用CDLL的構造函數建立一個CDLL對象。

>>> from ctypes import *
>>> mylib = CDLL('/home/yanxurui/test/keepcoding/python/extension/ctypes/libadd.so')

  第二行的CDLL等價於cdll.LoadLibrary

若是共享庫不在標準路徑/usr/lib下則須要使用完整的路徑。 ctypes提供了find_library用來找到共享庫的位置,可是find_library會查找/usr/local/lib,所以搜索成功不表明也能加載成功。有人也反映了這個bug:

CDLL does not use the same paths as find_library and thus you can find a library, but you can't necessarily use it.

調用共享庫裏的函數

經過訪問dll對象的屬性來調用相應的函數,就像調用python的函數對象同樣:

>>> mylib.add
<_FuncPtr object at 0x7ff6864b7bb0>
>>> add = mylib.add
>>> add(1,2)
3
>>> add()
1
>>> add(1)
-2044290911
>>> add(1,'a')                                                                                                    
-2042137139

指定函數類型

ctypes並不會校驗參數的數量和類型,經過設置函數的argtypes的屬性能夠指定函數參數的類型:

>>> add.argtypes = [c_int, c_int]
>>> add(1, 2)
3
>>> add(1)
Traceback (most recent call last):
  ...
TypeError: this function takes at least 2 arguments (1 given)
>>> add(1, '2')
Traceback (most recent call last):
  ...
ctypes.ArgumentError: argument 2: <type 'exceptions.TypeError'>: wrong type

  

另外,原生的python類型中只容許傳入None, 整數, 字符串做爲函數的參數。若是須要傳遞其餘的類型,則須要使用ctypes定義的類型,好比c_double表示double。

benchmark

從上面看出,c擴展雖然複雜,但更接地氣,性能必然也是最好的,而Cython和ctypes開發效率奇高。

調用C庫的一個主要目的是優化性能,所以咱們更關心三種方式對性能的影響。 下面經過一個簡單的benchmark來比較,即便10000000次加法操做也很快,很難看出調用C函數對性能帶來的提高,但這無所謂,由於咱們的主要目的是對比不一樣調用方式在調用共享庫時的性能開銷。

測試的代碼以下,因爲模塊名以及import的方式不一樣,因此每次測試須要稍微修改一下注釋的地方。

from time import time
# c ext
# from demo import add

# Cython
# from add_wrapper import add

# ctypes
# mylib = CDLL('/home/yanxurui/test/keepcoding/python/extension/ctypes/libadd.so')
# add = mylib.add
# add.argtypes = [c_int, c_int]

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

s=time()
for i in range(10000000):
    r = add(i, i)
print(time()-s)

  

10000000 loops, best of 3:

method cost(s)
c ext 2.522
Cython 1.723
ctypes 8.896
python 1.879

測試的結果讓人驚訝:

  1. 純python比c擴展快?
  2. Cython的調用開銷竟然比C模塊還低,這個是爲什麼???
  3. 使用ctypes調用C庫竟然比純python慢這麼多

對於這個測試的結果,我沒法盲目的相信,還須要進一步的探究。

總結

若是已經有一個現成的庫,我會選擇使用Cython或ctypes做爲包裝器,若是還須要考慮性能的話,固然就是Cython了。

相關文章
相關標籤/搜索