CFFI 是Python的C语言外部函数接口。通过CFFI,Python可以与C语言代码进行交互,使用起来也比较方便。
本文主要内容有:
- CFFI数组的基本使用
- CFFI数组和Numpy ndarry相互转换
测试环境:Python3.7.4(x64) + CFFI 1.3.1 + VS2019
安装CFFI
运行pip install cffi即可。
测试:
import cffi
print(cffi.__version__)
1.13.1
CFFI数组基本使用
首先导入库文件并实例化FFI
import numpy as np
from cffi import FFI
ffi = FFI()
CFFI数组使用
ffi.new
ffi.new(cdecl, init=None)根据指定的C语言类型创建实例并返回指针,如果C语言类型是数组,则返回它的引用。具体介绍见ffi.new。
例如:
a = ffi.new("int[]", 10)
print(len(a), a, type(a))
10 <cdata 'int[]' owning 40 bytes> <class '_cffi_backend.CDataOwn'>
对于返回的<cdata>指针,可以直接通过下标对其读取和赋值,也可以进行切片操作。使用切片时,注意不能省略开始和结束位置,不能使用负索引,不能使用步长。
# 通过下标读取赋值
a[0] = 11
a[1] = 55
for i in range(len(a)):
print(a[i], end=' ')
11 55 0 0 0 0 0 0 0 0
为了简便,后续<cdata>数组打印,直接将其转换为list。
切片
<cdata>数组的切片是对原数组的引用,它们共用相同的内存,对新切片进行赋值操作会影响原数组,这和Python的list切片操作有很大的不同。
# 切片赋值
a = ffi.new("int[]", 10)
a[3:5]=[11,22] # 长度必须一致
print(list(a))
# 切片引用
b = a[5:10]
print(b)
b[2] = 1200
print(list(a), list(b))
[0, 0, 0, 11, 22, 0, 0, 0, 0, 0]
<cdata 'int[]' sliced length 5>
[0, 0, 0, 11, 22, 0, 0, 1200, 0, 0] [0, 0, 1200, 0, 0]
从上面可以看出,数组a的切片也是<cdata>类型,对b的赋值操作会影响a数组。
多维数组
多维数组最简单的定义方式是定义时直接指定数组大小,可以避免复杂的初始化。例如二维数组使用ffi.new("int[2][10]"),三维数组使用ffi.new("int[2][5][10]")。使用len获取多维数组长度时,返回的是第一维的长度,例如:
b = ffi.new("int[2][5][10]")
print(b)
print(len(b), len(b[0]), len(b[0][0]))
<cdata 'int[2][5][10]' owning 400 bytes>
2 5 10
多维数组同样是通过下标进行读取赋值:
b[0][0][0] = 100
b[0][0][1:3] = [2,3]
print(list(b[0][0]))
[100, 2, 3, 0, 0, 0, 0, 0, 0, 0]
CFFI数组解包
ffi.unpack
ffi.unpack(cdata, length)解包指定长度的C语言数组,并返回一个list。对于一维数组,ffi.unpack和list()的效果类似。从测试结果来看,当数组长度较小时,list()的性能好一些;数据量变大时,ffi.unpack就更具优势,推荐编程时使用ffi.unpack对数组进行解包操作。
a = ffi.new("int[]", 10)
a[1] = 100
b = ffi.unpack(a, 10)
print(b, type(b))
print(b == list(a))
%timeit ffi.unpack(a, 10)
%timeit list(a)
a = ffi.new("int[]", 10000)
%timeit ffi.unpack(a, 10000)
%timeit list(a)
[0, 100, 0, 0, 0, 0, 0, 0, 0, 0] <class 'list'>
True
451 ns ± 36 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
265 ns ± 4.4 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
54.2 µs ± 2.05 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
74.8 µs ± 294 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
对于多维数组,例如a = ffi.new("int[4][10]"),解包时需要指定一维数组长度,使用时注意长度不要越界。
from pprint import pprint
a = ffi.new("int[4][10]")
a[3][9] = 1000
b = ffi.unpack(a,5)
pprint(b)
# 解包a[3]
b3 = ffi.unpack(b[3],10)
pprint(b3)
# 或者直接解包a[3]
b3 = ffi.unpack(a[3],10)
pprint(b3)
[<cdata 'int[10]' 0x0000027A3AA28548>,
<cdata 'int[10]' 0x0000027A3AA28570>,
<cdata 'int[10]' 0x0000027A3AA28598>,
<cdata 'int[10]' 0x0000027A3AA285C0>,
<cdata 'int[10]' 0x0000027A3AA285E8>]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1000]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1000]
可以看到,ffi.unpack操作返回一个list,其元素是<cdata>数组。多维数组的解包不是很直观,使用也不多,实际使用时,可以将其转换为Numpy ndarry。
CFFI数组和Numpy 相互转换
ndarray转CFFI数组
CFFI提供了ffi.from_buffer([cdecl,] python_buffer, require_writable=False)函数,可以把python_buffer转换为<cdata>数组。其中cdecl为python_buffer类型,默认为<cdata 'char[]'>。require_writable如果为True,则缓冲区python_buffer必须可写,否则函数会调用失败,具体介绍见ffi.from_buffer。
ndarray转CFFI数组:
a = np.arange(10, dtype=np.int)
# ndarray to cdata
b = ffi.from_buffer("int*", a)
print(b)
print(ffi.unpack(b, 10)) # 由于b是"int *"类型,不能使用list()
b[3] = 300
print(a, ffi.unpack(b, 10))
<cdata 'int *' buffer from 'numpy.ndarray' object>
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[ 0 1 2 300 4 5 6 7 8 9] [0, 1, 2, 300, 4, 5, 6, 7, 8, 9]
把ndarray转换为<cdata>数组时,它们共用同一块内存,对b的赋值操作也会影响数组a。
另外可以使用ffi.cast函数,把ndarray转换为<cdata>数组,例如:
b = ffi.cast("int*", a.ctypes.data)
其效果和ffi.from_buffer一样。
CFFI数组转ndarray
CFFI数组转ndarray有两步:
- 先用ffi.buffer(cdata, [size])把CFFI数组转换成python buffer。需要注意
size是字节长度。 - 再用numpy.frombuffer把python buffer转换成ndarray。实际使用时,需要指定
dtype和count。
例如:
a = np.arange(10, dtype=np.int)
# ndarray to cdata
b = ffi.from_buffer("int*", a)
# cdata to buffer
c = ffi.buffer(b, 40)
# buffer to ndarray
d = np.frombuffer(c, dtype=np.int, count=10)
print(d)
d[1] = 100
print(a, d)
[0 1 2 3 4 5 6 7 8 9]
[ 0 100 2 3 4 5 6 7 8 9] [ 0 100 2 3 4 5 6 7 8 9]
可以看到,a和d对应的是同一块内存。
下面使用float型完整介绍一遍CFFI数组和ndarray的相互转换:
a = np.arange(10, dtype=np.float32)
# ndarray to cdata
b = ffi.from_buffer("float*", a)
# cdata to buffer
c = ffi.buffer(b, 40) # 10 * 4 = 40
# buffer to ndarray
d = np.frombuffer(c, dtype=np.float32, count=10) # 数据类型需要对应
print(d)
d[1] = 100.0
print(a, d)
[0. 1. 2. 3. 4. 5. 6. 7. 8. 9.]
[ 0. 100. 2. 3. 4. 5. 6. 7. 8. 9.] [ 0. 100. 2. 3. 4. 5. 6. 7. 8. 9.]
以上就是本文的内容,后续会介绍下cffi、cython和ctypes性能对比。
本文代码地址:https://github.com/txfly/cffi_cython/archive/master.zip
版权声明:本文为「txfly」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://www.jianshu.com/p/80e0423ee70e