本文档由小小明个人学习整理
文章链接:https://blog.csdn.net/as604049322/article/details/112058313
pdf下载地址:https://download.csdn.net/download/as604049322/13999212
python调用go语言
Python是一个生产力很高的语言,能够以最高的效率完成最多的事,但是Python的性能,是我们一直诟病的一个问题,尤其是一个大锁GIL。当然现在大部分程序都是(IO)网络密集型程序,Python足以胜任,但是如果说我们已经存在的项目或者想要开发的项目中,存在有计算密集型的程序场景,该如何提升性能呢?
一般是可以用C\C++重写Python计算密集的地方,来提高性能,但是C\C++是有一些学习成本的,指针和自己释放内存都有一定门槛。Go就很方便了,自动垃圾自动回收,还有天生高并发等优势。
python的ctypes模块提供了和C语言兼容的数据类型和函数来加载so/dll动态链接库文件,而GO语言本身就可以编译出符合c语言规范的dll或so动态链接库,基于这两项特性,于是我们可以顺利的使用python来调用go语言。
Golang环境配置
Go官方镜像站点:https://golang.google.cn/dl/
选择默认的最高版本就好,Go代码向下兼容,版本之间的差异并无所谓
查看是否安装成功
>go version
go version go1.15.2 windows/amd64
注:由于已经是1.11+版本,我们以后使用go mod
进行管理依赖,不需要配置GOPATH等奇怪的东西。
配置GOPROXY(代理)
可能我们需要借用Go下载一些包什么的,但是默认官网源GOPROXY=https://proxy.golang.org,direct
,在国内访问不到
输入go env
查看Go配置:
>go env
...
set GOPROXY=https://proxy.golang.org,direct
set GOROOT=D:\Go
...
改成国内镜像站点:
go env -w GOPROXY=https://goproxy.cn,direct
再次查看Go配置:
>go env
...
set GOPROXY=https://goproxy.cn,direct
set GOROOT=D:\Go
...
go语言跨平台编译
跨平台编译,也叫交叉编译,我可以在win平台上,编译成linux平台可执行的文件。
这也是Go备受青睐的原因,像java,python,php等语言,我们开发一般是在win平台上开发,部署的时候在linux上部署,在处理第三方依赖是比较麻烦,不仅开发累,运维也累,虽然现在有docker解决了这个痛点,但是应该还是没原生来的舒服。
如果使用Go的话,不管是什么第三方依赖,最终只会打包成一个可执行文件,直接部署即刻,并且是高并发方式,心再大一点,连Nginx都不用,但是一点不用担心并发问题。
示例
Windows下编译linux平台可执行程序:
cmd
下依次执行以下命令:
SET CGO_ENABLED=0 // 禁用CGO
SET GOOS=linux // 目标平台是linux
SET GOARCH=amd64 // 目标处理器架构是amd64
然后执行go build
,得到的就是能够在linux上的可执行文件。
将这个文件上传到linux服务器上,即使Go环境都没有,都可以执行成功。
Windows下编译Mac平台64位可执行程序:
SET CGO_ENABLED=0
SET GOOS=darwin
SET GOARCH=amd64
go build
Mac 下编译 Linux 和 Windows平台 64位 可执行程序:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build
Linux 下编译 Mac 和 Windows 平台64位可执行程序:
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build
python与go性能对比
为了更好的体现出来优化之后的效果,我们大概对比一下两个语言在计算密集情况下的差距。
测试:分别计算一个亿(100000000)的累加模拟大量计算。
Python代码:
import time
def run(n):
sum = 0
for i in range(n):
sum += i
print(sum)
if __name__ == '__main__':
startTime = time.time()
run(100000000)
endTime = time.time()
print("耗时:", endTime - startTime)
耗时5s左右:
Go代码:
package main
import (
"fmt"
"time"
)
func run(n int) {
sum := 0
for i := 0; i < n; i++ {
sum += i
}
fmt.Println(sum)
}
func main() {
var startTime = time.Now()
run(100000000)
fmt.Println("耗时:", time.Since(startTime))
}
耗时50ms左右:
Go代码编译为Python可调用的.so文件
安装64位gcc工具MinGW
去https://sourceforge.net/projects/mingw-w64/下载后,一步步安装
已经将离线包上传到了百度云:
https://pan.baidu.com/s/1ZmjQUf5QcBbeHCi7mIrYxg 提取码: edc5
Windows适应于x86_64-8.1.0-release-win32-seh-rt_v6-rev0,直接解压并将MinGW的bin目录加入环境变量中后即可使用。
查看gcc版本:
>gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=D:/develop/mingw64/bin/../libexec/gcc/x86_64-w64-mingw32/8.1.0/lto-wrapper.exe
Target: x86_64-w64-mingw32
Configured with: ../../../src/gcc-8.1.0/configure --host=x86_64-w64-mingw32 --build=x86_64-w64-mingw32 --target=x86_64-w64-mingw32 --prefix=/mingw64 --with-sysroot=/c/mingw810/x86_64-810-win32-seh-rt_v6-rev0/mingw64 --enable-shared --enable-static --disable-multilib --enable-languages=c,c++,fortran,lto --enable-libstdcxx-time=yes --enable-threads=win32 --enable-libgomp --enable-libatomic --enable-lto --enable-graphite --enable-checking=release --enable-fully-dynamic-string --enable-version-specific-runtime-libs --disable-libstdcxx-pch --disable-libstdcxx-debug --enable-bootstrap --disable-rpath --disable-win32-registry --disable-nls --disable-werror --disable-symvers --with-gnu-as --with-gnu-ld --with-arch=nocona --with-tune=core2 --with-libiconv --with-system-zlib --with-gmp=/c/mingw810/prerequisites/x86_64-w64-mingw32-static --with-mpfr=/c/mingw810/prerequisites/x86_64-w64-mingw32-static --with-mpc=/c/mingw810/prerequisites/x86_64-w64-mingw32-static --with-isl=/c/mingw810/prerequisites/x86_64-w64-mingw32-static --with-pkgversion='x86_64-win32-seh-rev0, Built by MinGW-W64 project' --with-bugurl=https://sourceforge.net/projects/mingw-w64 CFLAGS='-O2 -pipe -fno-ident -I/c/mingw810/x86_64-810-win32-seh-rt_v6-rev0/mingw64/opt/include -I/c/mingw810/prerequisites/x86_64-zlib-static/include -I/c/mingw810/prerequisites/x86_64-w64-mingw32-static/include' CXXFLAGS='-O2 -pipe -fno-ident -I/c/mingw810/x86_64-810-win32-seh-rt_v6-rev0/mingw64/opt/include -I/c/mingw810/prerequisites/x86_64-zlib-static/include -I/c/mingw810/prerequisites/x86_64-w64-mingw32-static/include' CPPFLAGS=' -I/c/mingw810/x86_64-810-win32-seh-rt_v6-rev0/mingw64/opt/include -I/c/mingw810/prerequisites/x86_64-zlib-static/include -I/c/mingw810/prerequisites/x86_64-w64-mingw32-static/include' LDFLAGS='-pipe -fno-ident -L/c/mingw810/x86_64-810-win32-seh-rt_v6-rev0/mingw64/opt/lib -L/c/mingw810/prerequisites/x86_64-zlib-static/lib -L/c/mingw810/prerequisites/x86_64-w64-mingw32-static/lib '
Thread model: win32
gcc version 8.1.0 (x86_64-win32-seh-rev0, Built by MinGW-W64 project)
需要被编译.so文件的go代码有些要求,例如必须导入C:
package main
import (
"C" //C必须导入
)
//export run
func run(n int) int{
// 必须通过export 函数名格式的注释申明该函数可以被外部接口
sum := 0
for i := 0; i < n; i++ {
sum += i
}
fmt.Println("我是Go代码,我跑完了,我的结果是:",sum)
return sum
}
func main() {
//main函数中什么都不要写,和包名main要对应
}
编译为.so文件供Python调用:
go build -buildmode=c-shared -o s1.so s1.go
格式:go build -buildmode=c-shared -o 输出的.so文件 go源文件
会生成.h文件和.so文件,.so文件供Python调用,如下图所示:
Ptyhon调用so文件
将上述生成的.so文件复制到Python项目的同一级目录。
编写s1.py,依然是计算一个亿,关键部分由Go生成的.so执行:
from ctypes import *
import time
if __name__ == '__main__':
startTime = time.time()
s = CDLL("s1.so") # 加载s1.so文件
result = s.run(100000000) # 调用Go生成的.so文件里面的run函数
print("result:", result)
endTime = time.time()
print("耗时:", endTime - startTime)
共耗时:0.04s左右:
可以看到,虽然速度很快,但是Python在调用Go生成的.so文件之后,拿到的返回值竟然是错的,但是在Go中打印的确实对的!
但是计算一些的比较小的数,以10023为例,结果是正确的:
.h文件探究
上面的问题是因为默认返回值类型存储范围有限导致的,下面将具体分析go编译生成的c中间文件一探究竟。
打开.h文件,翻到末尾:
找到extern开头的声明:
extern GoInt run(GoInt n);
这是前面go源码中声明的run方法被转换为c语言代码,表示参数和返回值类型在c语言中都是GoInt类型。
翻到类型定义的位置:
可以看到,GoInt其实就是GoInt64,GoInt64的类型是long long类型。
Python使用ctypes
模块调用.so文件时有一个对应表:
参考:https://docs.python.org/zh-tw/3.7/library/ctypes.html
ctypes 类型 | C 类型 | Python 类型 |
---|---|---|
c_bool | _Bool | bool (1) |
c_char | char | 单字符字节对象 |
c_wchar | wchar_t | 单字符字符串 |
c_byte | char | int |
c_ubyte | unsigned char | int |
c_short | short | int |
c_ushort | unsigned short | int |
c_int | int | int |
c_uint | unsigned int | int |
c_long | long | int |
c_ulong | unsigned long | int |
c_longlong | __int64 或 long long | int |
c_ulonglong | unsigned __int64 或 unsigned long long | int |
c_size_t | size_t | int |
c_ssize_t | ssize_t 或 Py_ssize_t | int |
c_float | float | float |
c_double | double | float |
c_longdouble | long double | float |
c_char_p | char * (以 NUL 结尾) | 字节串对象或 None |
c_wchar_p | wchar_t * (以 NUL 结尾) | 字符串或 None |
c_void_p | void * | int 或 None |
根据上述表格可以发现,在C中的long long类型对应的ctype类型是c_longlong,在python中的类型是int。
python的默认数值处理类型是Long(8字节),go语言编译的run方法,未申明的情况下返回值类型却是Int(4字节),所以当计算结果超过Int的可存储范围时就会出现问题。
Int的取值范围为:-2^31 — 2^31-1,即-2147483648 — 2147483647
现在根据实际的ctype在python中申明run的实际返回值类型即可:
from ctypes import *
import time
if __name__ == '__main__':
beginTime = time.time()
s = CDLL("s1.so") # 加载s1.so文件
# 根据查表,C中的long long,对应的ctypes 是 c_longlong
s.run.restype = c_longlong # 声明.so的run函数返回值类型,固定格式
result = s.run(100000000) # 调用Go生成的.so文件里面的run函数
print(result)
endTime = time.time()
print("耗时:", endTime - beginTime)
现在结果就没有问题了。
处理返回值为字符串的情况
s2.go的代码:
package main
import (
"C" //C必须导入
)
//export speak
func speak(n int) string {
return "996好累呀,难得休息一天,好好休息 "
}
func main() {
//main函数中什么都不要写,和包名main要对应
}
查看s2.h:
typedef struct { const char *p; ptrdiff_t n; } _GoString_;
typedef _GoString_ GoString;
...
extern GoString speak(GoInt n);
...
上面表示GoString是_GoString_
类型,而_GoString_
是char *和ptrdiff_t的结构体
在c语言规范中,ptrdiff_t是C/C++标准库中定义的一个与机器相关的数据类型。ptrdiff_t类型变量通常用来保存两个指针减法操作的结果。ptrdiff_t定义在stddef.h(cstddef)这个文件内。ptrdiff_t通常被定义为long int类型,可以被定义为long long类型。
查表可知,在python中应申明c_char_p和c_longlong的结构体:
class GoString(Structure):
# typedef struct { const char *p; ptrdiff_t n; } _GoString_;
# ptrdiff_t == long long
_fields_ = [("p", c_char_p), ("n", c_longlong)]
s3.py完整代码:
from ctypes import *
import time
class GoString(Structure):
# typedef struct { const char *p; ptrdiff_t n; } _GoString_;
_fields_ = [("p", c_char_p), ("n", c_longlong)]
if __name__ == '__main__':
beginTime = time.time()
s = CDLL("s2.so") # 加载s1.so文件
s.speak.restype = GoString
speakStr = s.speak(5)
# 返回的是字节类型,需要转字符串,返回的内容在.p中,.n是切的长度
speakStr = speakStr.p[:speakStr.n].decode("utf-8")
print("speak:", speakStr)
endTime = time.time()
print("耗时:", endTime - beginTime)
但是上面的这种代码只支持返回的字符串为常量,一旦我将go代码修改为以下内容再重复以上步骤时:
s2.go代码:
package main
import (
"C" //C必须导入
"strconv"
)
//export speak
func speak(n int) string {
s := "996好累呀,难得休息一天,好好休息 " + strconv.Itoa(n)
return s
}
func main() {
//main函数中什么都不要写,和包名main要对应
}
重复以上步骤,运行s3.py,出现如下错误:
这是因为此时的string不是c语言的对象,而是go语言的对象,修改为如下代码即可:
s2.go代码:
package main
import (
"C" //C必须导入
"strconv"
)
//export speak
func speak(n int) *C.char {
s := "996好累呀,难得休息一天,好好休息 " + strconv.Itoa(n)
return C.CString(s)
}
func main() {
//main函数中什么都不要写,和包名main要对应
}
以上代码申明返回c语言的字符串类型,查看.h文件可以看到:
extern char* speak(GoInt n);
那么s3.py代码只需修改为:
from ctypes import *
import time
if __name__ == '__main__':
beginTime = time.time()
s = CDLL("s3.so") # 加载s1.so文件
s.speak.restype = c_char_p
speakStr = s.speak(7).decode("utf-8")
print("speak:", speakStr)
endTime = time.time()
print("耗时:", endTime - beginTime)
顺利运行。
使用ctypes访问C代码
基本示例
实现两数求和的C代码add.c文件:
#include <stdio.h>
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;
}
将C文件编译为.so
文件:
#For Linux or windows
gcc -shared -Wl,-soname,adder -o adder.so -fPIC add.c
#For Mac
gcc -shared -Wl,-install_name,adder.so -o adder.so -fPIC add.c
在Python代码中来调用它:
from ctypes import *
adder = CDLL('adder.so')
res_int = adder.add_int(4, 5)
print("Sum of 4 and 5 = " + str(res_int))
add_float = adder.add_float
add_float.restype = c_float
a = c_float(5.5)
b = c_float(4.1)
print("Sum of 5.5 and 4.1 = ", str(add_float(a, b)))
输出:
Sum of 4 and 5 = 9
Sum of 5.5 and 4.1 = 9.600000381469727
ctypes接口允许我们在调用C函数时参数使用原生Python中默认的字符串型和整型,而对于其他类似布尔型和浮点型这样的类型,必须要使用正确的ctype类型才可以。如向adder.add_float()
函数传参时, 要先将Python中的float类型转化为c_float类型,然后才能传送给C函数。
复杂示例
编码c代码,sample.c文件的内容为:
#include <math.h>
int gcd(int x, int y) {
int g = y;
while (x > 0) {
g = x;
x = y % x;
y = g;
}
return g;
}
int in_mandel(double x0, double y0, int n) {
double x = 0, y = 0, xtemp;
while (n > 0) {
xtemp = x * x - y * y + x0;
y = 2 * x * y + y0;
x = xtemp;
n -= 1;
if (x * x + y * y > 4) return 0;
}
return 1;
}
int divide(int a, int b, int *remainder) {
int quot = a / b;
*remainder = a % b;
return quot;
}
double avg(double *a, int n) {
int i;
double total = 0.0;
for (i = 0; i < n; i++) {
total += a[i];
}
return total / n;
}
typedef struct Point {
double x, y;
} Point;
double distance(Point *p1, Point *p2) {
return hypot(p1->x - p2->x, p1->y - p2->y);
}
命令行执行以下代码,编译c:
gcc -shared -o sample.so sample.c
在 sample.so 所在文件相同的目录编写python代码,sample.py文件:
import ctypes
_mod = ctypes.cdll.LoadLibrary('sample.so')
# int gcd(int, int)
gcd = _mod.gcd
gcd.argtypes = (ctypes.c_int, ctypes.c_int)
gcd.restype = ctypes.c_int
# int in_mandel(double, double, int)
in_mandel = _mod.in_mandel
in_mandel.argtypes = (ctypes.c_double, ctypes.c_double, ctypes.c_int)
in_mandel.restype = ctypes.c_int
# int divide(int, int, int *)
_divide = _mod.divide
_divide.argtypes = (ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.c_int))
_divide.restype = ctypes.c_int
def divide(x, y):
rem = ctypes.c_int()
quot = _divide(x, y, rem)
return quot, rem.value
# void avg(double *a, int n)
# 定义 'double *'参数的类型
class DoubleArrayType:
def from_param(self, param):
typename = type(param).__name__
if hasattr(self, 'from_' + typename):
return getattr(self, 'from_' + typename)(param)
elif isinstance(param, ctypes.Array):
return param
else:
raise TypeError("Can't convert %s" % typename)
# Cast from array.array objects
def from_array(self, param):
if param.typecode != 'd':
raise TypeError('must be an array of doubles')
ptr, _ = param.buffer_info()
return ctypes.cast(ptr, ctypes.POINTER(ctypes.c_double))
# Cast from lists/tuples
def from_list(self, param):
val = ((ctypes.c_double) * len(param))(*param)
return val
from_tuple = from_list
# Cast from a numpy array
def from_ndarray(self, param):
return param.ctypes.data_as(ctypes.POINTER(ctypes.c_double))
_avg = _mod.avg
_avg.argtypes = (DoubleArrayType(), ctypes.c_int)
_avg.restype = ctypes.c_double
def avg(values):
return _avg(values, len(values))
# struct Point { }
class Point(ctypes.Structure):
_fields_ = [('x', ctypes.c_double),
('y', ctypes.c_double)]
# double distance(Point *, Point *)
distance = _mod.distance
distance.argtypes = (ctypes.POINTER(Point), ctypes.POINTER(Point))
distance.restype = ctypes.c_double
然后就可以加载并使用里面定义的C函数了,编写test.py:
import sample
print("sample.gcd(35, 42):", sample.gcd(35, 42))
print("sample.in_mandel(0, 0, 500):", sample.in_mandel(0, 0, 500))
print("sample.in_mandel(2.0, 1.0, 500):", sample.in_mandel(2.0, 1.0, 500))
print("sample.divide(42, 8):", sample.divide(42, 8))
print("sample.avg([1, 2, 3]):", sample.avg([1, 2, 3]))
p1 = sample.Point(1, 2)
p2 = sample.Point(4, 5)
print("sample.distance(p1, p2):", sample.avg([1, 2, 3]))
执行结果:
sample.gcd(35, 42): 7
sample.in_mandel(0, 0, 500): 1
sample.in_mandel(2.0, 1.0, 500): 0
sample.divide(42, 8): (5, 2)
sample.avg([1, 2, 3]): 2.0
sample.distance(p1, p2): 2.0
复杂示例解析
加载c函数库
如果C函数库被安装为一个标准库,那么可以使用 ctypes.util.find_library() 函数来查找它所在的位置:
>>> from ctypes.util import find_library
>>> find_library('m')
'libm.so.6'
>>> find_library('pthread')
'libpthread.so.0'
>>> find_library('sample')
如果是非标准库,则需要知道C函数库的位置,然后使用 ctypes.cdll.LoadLibrary() 来加载它:
_mod = ctypes.cdll.LoadLibrary(_path) #_path是C函数库的位置,全路径和相对路径都可以
指定参数和返回值的类型
函数库被加载后,需要提取特定的符号指定它们的类型。例如:
# int in_mandel(double, double, int)
in_mandel = _mod.in_mandel
in_mandel.argtypes = (ctypes.c_double, ctypes.c_double, ctypes.c_int)
in_mandel.restype = ctypes.c_int
这段代码中,函数的.argtypes
属性是一个元组,包含了某个函数的输入参数,而 .restype
是函数的返回类型。
ctypes 定义的c_double, c_int, c_short, c_float等代表了对应的C数据类型。
为了让Python能够传递正确的参数类型并且正确的转换数据,这些类型签名的绑定是很重要的一步。如果省略这个类型签名的步骤,可能导致代码不能正常运行,甚至整个解释器进程挂掉。
指针参数需要以ctypes对象形式传入
原生的C代码的类型有时跟Python不能明确的对应上来,例如:
# c代码中的int divide(int, int, int *)
_divide = _mod.divide
_divide.argtypes = (ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.c_int))
_divide.restype = ctypes.c_int
# python代码中的调用
x = 0
divide(10, 3, x)
这种写法违反了Python对于整数的不可更改原则,并且可能会导致整个解释器陷入一个黑洞中。
对于涉及到指针的参数,通常需要先构建一个相应的ctypes对象再作为参数传入:
x = ctypes.c_int()
divide(10, 3, x)
ctypes.c_int 实例是作为指针被传进去的,跟普通Python整数不同的是,c_int 对象是可以被修改的。
.value
属性可被用来获取或更改这个值:
x.value
对于这种不像Python的C调用,通常可以写一个包装函数:
# int divide(int, int, int *)
_divide = _mod.divide
_divide.argtypes = (ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.c_int))
_divide.restype = ctypes.c_int
def divide(x, y):
rem = ctypes.c_int()
quot = _divide(x, y, rem)
return quot, rem.value
参数包含数组
对于avg()函数,double avg(double *a, int n)
,C代码期望接受到一个double类型的数组指针和一个数组的长度值。
在Python中数组有多种形式,包括列表、元组、array 模块的数组、 numpy 数组等。
DoubleArrayType 演示了怎样处理这种情况。
方法 from_param() 接受一个单个参数然后将其向下转换为一个合适的ctypes对象:
def from_param(self, param):
typename = type(param).__name__
if hasattr(self, 'from_' + typename):
return getattr(self, 'from_' + typename)(param)
elif isinstance(param, ctypes.Array):
return param
else:
raise TypeError("Can't convert %s" % typename)
参数的类型名被提取出来并被用于分发到一个更具体的方法中去。
例如,如果参数是一个列表,那么 typename 就是 list ,然后 from_list 方法就会被调用。
def from_list(self, param):
val = ((ctypes.c_double) * len(param))(*param)
return val
演示通过交互式命令行将list列表转换为 ctypes 数组:
>>> import ctypes
>>> nums = [1, 2, 3]
>>> a = (ctypes.c_double * len(nums))(*nums)
>>> a
<__main__.c_double_Array_3 object at 0x10069cd40>
>>> a[0]
1.0
>>> a[1]
2.0
>>> a[2]
3.0
如果参数是一个numpy数组,那么 typename 就是 ndarray,然后 from_ndarray方法就会被调用:
def from_ndarray(self, param):
return param.ctypes.data_as(ctypes.POINTER(ctypes.c_double))
如果参数是一个数组对象,那么 typename 就是 array,然后 from_array方法就会被调用:
def from_array(self, param):
if param.typecode != 'd':
raise TypeError('must be an array of doubles')
ptr, _ = param.buffer_info()
return ctypes.cast(ptr, ctypes.POINTER(ctypes.c_double))
对于数组对象,buffer_info()方法可以获取到数组对应的内存地址和长度,ctypes.cast()可以将内存地址转换为ctypes 指针对象:
>>> import array
>>> a = array.array('d',[1,2,3])
>>> a
array('d', [1.0, 2.0, 3.0])
>>> ptr,length = a.buffer_info()
>>> ptr
4298687200
>>> length
3
>>> ctypes.cast(ptr, ctypes.POINTER(ctypes.c_double))
<__main__.LP_c_double object at 0x10069cd40>
通过定义 DoubleArrayType类并在 avg() 类型签名中使用它,那么这个函数就能接受多个不同的类数组输入了:
import sample
sample.avg([1,2,3])
2.0
sample.avg((1,2,3))
2.0
import array
sample.avg(array.array('d',[1,2,3]))
2.0
import numpy
sample.avg(numpy.array([1.0,2.0,3.0]))
2.0
参数包含结构体
对于结构体,只需要简单的定义一个类,包含相应的字段和类型即可:
class Point(ctypes.Structure):
_fields_ = [('x', ctypes.c_double),
('y', ctypes.c_double)]
类型签名绑定只需:
# double distance(Point *, Point *)
distance = _mod.distance
distance.argtypes = (ctypes.POINTER(Point), ctypes.POINTER(Point))
distance.restype = ctypes.c_double
一旦类被定义后,就可以在类型签名中或者是需要实例化结构体的代码中使用它。例如:
>>> p1 = sample.Point(1,2)
>>> p2 = sample.Point(4,5)
>>> p1.x
1.0
>>> p1.y
2.0
>>> sample.distance(p1,p2)
4.242640687119285
将函数指针转换为可调用对象
获取C函数的内存地址(经测试,在linux上支持,windows上不支持):
import ctypes
lib = ctypes.cdll.LoadLibrary(None)
# 获取C语言math库的sin()函数的地址
addr = ctypes.cast(lib.sin, ctypes.c_void_p).value
print(addr)
上述代码在linux下得到整数140266666308000,而在Windows下会报错TypeError: LoadLibrary() argument 1 must be str, not None
有了函数的内存地址,就可以将它转换成一个Python可调用对象:
# 将函数地址转换成一个Python的可调用对象,参数为函数的返回值类型和参数类型
functype = ctypes.CFUNCTYPE(ctypes.c_double, ctypes.c_double)
sin = functype(addr)
print(sin)
CFUNCTYPE() 的第一个参数是返回类型,接下来的参数是参数类型,生成的对象被当做普通的可通过 ctypes 访问的函数来使用。
打印:<CFunctionType object at 0x7f9261becb38>
调用测试:
>>> import math
>>> math.pi
3.141592653589793
>>> sin(math.pi)
1.2246467991473532e-16
>>> sin(math.pi/2)
1.0
>>> sin(math.pi/6)
0.49999999999999994
>>> sin(2)
0.9092974268256817
>>> sin(0)
0.0
这里面涉及的技术被广泛使用于各种高级代码生成技术,比如即时编译,在LLVM函数库中可以看到。
下面简单演示下 llvmpy 扩展,构建一个小的聚集函数,获取它的函数指针,然后转换为一个Python可调用对象,并执行函数:
>>> from llvm.core import Module, Function, Type, Builder
>>> mod = Module.new('example')
>>> f = Function.new(mod,Type.function(Type.double(), [Type.double(), Type.double()], False), 'foo')
>>> block = f.append_basic_block('entry')
>>> builder = Builder.new(block)
>>> x2 = builder.fmul(f.args[0],f.args[0])
>>> y2 = builder.fmul(f.args[1],f.args[1])
>>> r = builder.fadd(x2,y2)
>>> builder.ret(r)
<llvm.core.Instruction object at 0x10078e990>
>>> from llvm.ee import ExecutionEngine
>>> engine = ExecutionEngine.new(mod)
>>> ptr = engine.get_pointer_to_function(f)
>>> ptr
4325863440
>>> foo = ctypes.CFUNCTYPE(ctypes.c_double, ctypes.c_double, ctypes.c_double)(ptr)
>>> foo(2,3)
13.0
>>> foo(4,5)
41.0
>>> foo(1,2)
5.0
注意:这是在直接跟机器级别的内存地址和本地机器码打交道,而不是Python函数。
处理参数包含字符串的情况
测试程序str1.c:
#include <stdio.h>
void print_chars(char *s) {
printf("%s\n",s);
while (*s) {
printf("%2x ", (unsigned char) *s);
s++;
}
printf("\n");
}
int main() {
print_chars("Hello");
}
执行结果:
> gcc str1.c&a.exe
Hello
48 65 6c 6c 6f
编译c程序为so文件:
gcc -shared -o str1.so str1.c
用python调用:
import ctypes
_mod = ctypes.cdll.LoadLibrary('str1.so')
# void print_chars(char *s)
print_chars = _mod.print_chars
print_chars.argtypes = (ctypes.c_char_p,)
print_chars(b'Hello')
print_chars(b'Hello\x00World')
打印结果:
Hello
48 65 6c 6c 6f
Hello
48 65 6c 6c 6f
不能直接传入python的字符串类型,例如:print_chars('Hello World')
否则会报错:ctypes.ArgumentError: argument 1: <class 'TypeError'>: wrong type
如果需要传递字符串而不是字节,可以先编码成 UTF-8 转成字节:
>>> print_chars('Hello World'.encode('utf-8'))
Hello World
48 65 6c 6c 6f 20 57 6f 72 6c 64