python調用C語言


python由於在實現多線程的情況下,由於GIL(全局解釋器鎖)的存在,只能實現偽線程,要想真正實現多線程,可以調用第三方的擴展,使用C語言編寫一些需要實現多線程的業務邏輯。

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

c extension

介紹

python標准庫包含了很多使用C開發的擴展模塊,比如對性能要求很高的json庫。開發者同樣可以使用C開發擴展,這是最原始也是最底層的擴展python的方式。

示例

demomodule.c

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

  • 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])

  執行

python setup.py build_ext --inplace

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

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

 測試

>>> 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了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM