31-Python 和 C / C++ 联合编程 楔子 Python 和 C / C++ 混合编程已经屡见不鲜了,那为什么要将这两种语言结合起来呢?或者说,这两种语言混合起来能给为我们带来什么好处呢?首先,Python 和 C / C++ 联合,无非两种情况。
1. C / C++ 为主导的项目中引入 Python;
2. Python 为主导的项目中引入 C / C++;
首先是第一种情况,因为 C / C++ 是编译型语言,而它们的编译调试的成本是很大的。如果用 C / C++ 开发一个大型项目的话,比如游戏引擎,这个时候代码的修改、调试是无可避免的。而对于编译型语言来说,你对代码做任何一点改动都需要重新编译,而这个耗时是比较长的,所以这样算下来成本会非常高。这个时候一个比较不错的做法是,将那些跟性能无关的内容开放给脚本,可以是 Lua 脚本、也可以是 Python 脚本,而脚本语言不需要编译,我们可以随时修改,这样可以减少编译调试的成本。还有就是引入了 Python 脚本之后,我们可以把 C / C++ 做的更加模块化,由 Python 将 C / C++ 各个部分联合起来,这样可以降低 C / C++ 代码的耦合度,从而加强可重用性。
然后是第二种情况,Python 项目中引入 C / C++。我们知道 Python 的效率不是很高,如果你希望 Python 能够具有更高的性能,那么可以把一些和性能相关的逻辑使用 C / C++ 进行重写。此外,Python 有大量的第三方库,特别是诸如 Numpy、Pandas、Scipy 等等和科学计算密切相关的库,底层都是基于 C / C++ 的。再比如机器学习,底层核心算法都是基于 C / C++ 编写的,然后在业务层暴露给 Python 去调用,因此对于一些需要高性能的领域,Python 是必须要引入 C / C++ 的。此外 Python 还有一个最让人诟病的问题,就是由于 GIL 的限制导致 Python 无法有效利用多核,而引入 C / C++ 可以绕过 GIL 的限制。
此外有一个项目叫做 Cython,从名字你就能看出来这是将 Python 和 C / C++ 结合在了一起,之所以把它们结合在一起,很明显,因为这两者不是对立的,而是互补的。Python 是高阶语言、动态、易于学习,并且灵活。但是这些优秀的特性是需要付出代价的,因为 Python 的动态性、以及它是解释型语言,导致其运行效率比静态编译型语言慢了好几个数量级。而 C / C++ 是非常古老的静态编译型语言,并且至今也被广泛使用。从时间来算的话,其编译器已有将近半个世纪的历史,在性能上做了足够的优化。而 Cython 的出现,就是为了让你编写的代码具有 C / C++ 的高效率的同时,还能有 Python 的开发速度。
而笔者本人是主 Python 的,所以我们只会介绍第二种,也就是 Python 项目中引入 C / C++。而在 Python 中引入 C / C++,也涉及两种情况。第一种是,Python 通过 ctypes 模块直接调用 C / C++ 编写好的动态链接库,此时不会涉及任何的 Python / C API,只是单纯的通过 ctypes 模块将 Python 中的数据转成 C 中的数据传递给函数进行调用,调用完之后再将返回值转成 Python 中的数据。因此这种方式它和 Python 底层提供的 Python / C API 无关,和 Python 的版本也无关,因此会很方便。但很明显这种方式是有局限性的,至于局限性在哪儿,我们后面慢慢聊,因此还有一种选择是通过 C / C++ 为 Python 编写扩展模块的方式,来在 Python 中引入 C / C++,比如 OpenCV。
无论是 ctypes 调用动态链接库,还是 C / C++ 为 Python 编写扩展模块,我们都会介绍。
环境准备 首先是 Python 的安装,估计这应该不用我说了,我这里使用的 Python 版本是 3.8.7。
然后重点是 C / C++ 编译器的安装,我这里使用的是 64 位的 Windows 10 操作系统,所以我们需要手动安装相应的编译环境。可以下载一个 gcc,然后配置到环境变量中,就可以使用了。
或者安装 Visual Studio,我的 Visual Studio 版本是 2017,在命令行中可以通过 cl 命令进行编译。
当然这两种命令的使用方式都是类似的,或者你也可以使用 Linux,比如 CentOS,基本上自带 gcc。当然 Linux 的话,环境什么的比较简单,这里就不再废话了。重点是如果你是在 Windows 上使用 Visual Studio 的话,在命令行中输入命令 cl,很可能会提示你命令找不到;再或者编译的时候,会提示你 fatal error 不包括路径集等等。出现以上问题的话,说明你的环境变量没有配置正确,下面来说一下环境变量的配置。再次强调,我操作系统是 64 位 Windows 10,Visual Studio 版本是 2017,相信大部分人应该我是一样的,如果完全一样的话,那么路径啥的应该也是一致的,当然最好还是检查一下。
首先在 path 中添加如下几个路径:
C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Tools\MSVC\14.16.27023\bin\Hostx64\x64
C:\Program Files (x86)\Windows Kits\10\bin\10.0.17763.0\x64
C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\IDE
然后,新建一个环境变量。
变量名为 LIB,变量值为以下路径,由于是写在一行,所以路径之间需要使用分号进行隔开。
C:\Program Files (x86)\Windows Kits\10\Lib\10.0.17763.0\um\x64
C:\Program Files (x86)\Windows Kits\10\Lib\10.0.17763.0\ucrt\x64
C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Tools\MSVC\14.16.27023\lib\x64
最后,还是新建一个环境变量,变量名为 INCLUDE,变量值为以下路径:
C:\Program Files (x86)\Windows Kits\10\Include\10.0.17763.0\ucrt
C:\Program Files (x86)\Windows Kits\10\Lib\10.0.17763.0\um
C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Tools\MSVC\14.16.27023\include
以上就是 Windows 系统中配置 Visual Studio 2017 环境变量的整个过程,配置完毕之后重启命令行之后就可以使用了。注意:以上是我当前机器的路径,如果你的配置和我不一样,记得仔细检查。
不过个人更习惯使用 gcc,因此后面我们会使用 gcc 进行编译。
Python ctypes 模块调用 C / C++ 动态链接库 通过 ctypes 模块(Python 自带的)调用 C / C++ 动态库,也算是 Python 和 C / C++ 联合编程的一种方案,而且是最简单的一种方案。因为它只对你的操作系统有要求,比如 Windows 上编译的动态库是 .dll 文件,Linux 上编译的动态库是 .so 文件,只要操作系统一致,那么任何提供了 ctypes 模块的 Python 解释器都可以调用。这种方式的使用场景是 Python 和 C / C++ 不需要做太多的交互,比如嵌入式设备,可能只是简单调用底层驱动提供的某个接口而已。
再比如我们使用 C / C++ 写了一个高性能的算法,然后通过 Python 的 ctypes 模块进行调用也是可以的,但我们之前说使用 ctypes 具有相应的局限性,这个局限性就是 C / C++ 提供的接口不能太复杂。因为 ctypes 提供的交互能力还是比较有限的,最明显的问题就是不同语言数据类型不同,一些复杂的交互方式还是比较难做到的,还有多线程的控制问题等等。
举个小栗子 首先我们来举个栗子,演示一下。
这是个简单到不能再简单的 C 函数,然后我们来编译成动态库。
1 编译方式: gcc -o .dll文件或者.so文件 -shared c或者c++源文件
如果你用的是 Visual Studio,那么把 gcc 换成 cl 即可。我当前的源文件叫做 main.c,我们编译成 main.dll,那么命令就需要这么写:gcc -o main.dll -shared main.c。
编译成功之后,我们通过 ctypes 来进行调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import ctypeslib = ctypes.CDLL(r"./main.dll" ) print (lib.f()) func = getattr (lib, "f" , None ) if func: print (func) func() func1 = getattr (lib, "f2" , None ) print (func1)
所以使用ctypes去调用动态链接库非常方便,过程很简单:
1. 通过 ctypes.CDLL 去加载动态库,另外注意的是:dll 或者 so 文件的路径最好是绝对路径,即便不是也要表明层级。比如我们这里的 py 文件和 dll 文件是在同一个目录下,但是我们加载的时候不可以写 main.dll,这样会报错找不到,我们需要写成 ./main.dll
2. 加载动态链接库之后会返回一个对象,我们上面起名为 lib,这个 lib 就是得到的动态链接库了
3. 然后可以直接通过 lib 调用里面的函数,但是一般我们会使用反射的方式来获取,因为不知道函数到底存不存在,如果不存在直接调用会抛出异常,如果存在这个函数我们才会调用。
Linux 和 Mac 也是一样的,这里不演示了,只不过编译之后的名字不一样。Linux 系统是 .so,Mac 系统是 .dylib。
此外我们也可以在 C 中进行打印,举个栗子:
1 2 3 4 5 #include <stdio.h> void f () { printf ("hello world" ); }
然后编译,进行调用。
1 2 3 4 import ctypeslib = ctypes.CDLL(r"./main.dll" ) lib.f()
另外,Python 的 ctypes 调用的都是 C 语言函数,如果你用的 C++ 编译器,那么会编译成 C++ 中的函数。我们知道 C 语言的函数不支持重载,说白了就是不可以定义两个同名的函数,而 C++ 的函数是支持重载的,只要参数类型不一致即可,然后调用的时候会根据传递的参数调用对应的函数。所以当我们使用 C++ 编译器的时候,需要通过 extern “C” 将函数包起来,这样 C++ 编译器在编译的时候会将其编译成 C 的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <stdio.h> #ifdef __cplusplus extern "C" {#endif void f () { printf ("hello world\n" ); } #ifdef __cplusplus } #endif
当然我们在介绍 ctypes 使用的 gcc 都是 C 编译器,会编译成 C 的函数,所以后面 extern “C” 的逻辑就不加了。
我们以上就演示了,如何通过 Python 的 ctypes 模块来调用 C / C++ 动态库,但显然目前还是远远不够的。比如说:
1 2 3 double f () { return 3.14 ; }
然后我们调用的时候,会得到什么结果呢?来试一下:
1 2 3 4 import ctypeslib = ctypes.CDLL(r"./main.dll" ) print (lib.f())
我们看到得到一个不符合预期的结果,我们暂且不纠结它是怎么来的,现在的问题是它返回的为什么不是 3.14 呢?原因是 ctypes 在解析的时候默认是按照整型来解析的,但很明显我们 C 函数返回是浮点型,因此我们在调用之前需要显式的指定其返回值。
不过在这之前,我们需要先来看看 Python 类型和 C 类型之间的转换关系。
Python 类型与 C 语言类型之间的转换 我们说可以使用 ctypes 调用动态链接库,主要是调用动态链接库中使用C编写好的函数,但这些函数肯定都是需要参数的,还有返回值,不然编写动态链接库有啥用呢。那么问题来了,不同的语言变量类型不同,所以 Python 能够直接往 C 编写的函数中传参吗?显然不行,因此 ctypes 提供了大量的类,帮我们将 Python 中的类型转成 C 语言中的类型。
我们说了,Python 中类型不能直接往 C 语言的函数中传递(整型是个例外),而 ctypes 可以帮助我们将 Python 的类型转成 C 类型。而常见的类型分为以下几种:数值、字符、指针。
数值类型转换 C 语言的数值类型分为如下:
int:整型
unsigned int:无符号整型
short:短整型
unsigned short:无符号短整型
long:长整形
unsigned long:无符号长整形
long long:64位机器上等同于 long
unsigned long long:等同于 unsigned long
float:单精度浮点型
double:双精度浮点型
long double:看成是 double 即可
_Bool:布尔类型
ssize_t:等同于 long 或者 long long
size_t:等同于 unsigned long 或者 unsigned long long
下面来演示一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import ctypesprint (ctypes.c_int(1 )) print (ctypes.c_uint(1 )) print (ctypes.c_short(1 )) print (ctypes.c_ushort(1 )) print (ctypes.c_long(1 )) print (ctypes.c_ulong(1 )) print (ctypes.c_longlong(1 )) print (ctypes.c_ulonglong(1 )) print (ctypes.c_float(1.1 )) print (ctypes.c_double(1.1 )) print (ctypes.c_longdouble(1.1 )) print (ctypes.c_bool(True )) print (ctypes.c_ssize_t(10 )) print (ctypes.c_size_t(10 ))
字符类型转换、指针类型转换 C 语言的字符类型分为如下:
char:一个 ascii 字符或者 -128~127 的整型
wchar:一个 unicode 字符
unsigned char:一个 ascii 字符或者 0~255 的一个整型
C 语言的指针类型分为如下:
char *:字符指针
wchar_t *:字符指针
void *:空指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import ctypesprint (ctypes.c_char(b"a" )) print (ctypes.c_char(97 )) print (ctypes.c_wchar("憨" )) print (ctypes.c_byte(97 )) print (ctypes.c_ubyte(97 )) print (ctypes.c_char_p(b"hello world" )) print (ctypes.c_wchar_p("憨八嘎~" ))
常见的类型就是上面这些,至于其他的类型,比如整型指针、数组、结构体、回调函数等等,ctypes 也是支持的,我们后面会介绍。
参数传递 下面我们来看看如何传递参数。
1 2 3 4 5 6 #include <stdio.h> void test (int a, float f, char *s) { printf ("a = %d, b = %.2f, s = %s\n" , a, f, s); }
这是一个很简单的 C 文件,然后编译成 dll 之后,让 Python 去调用,这里我们编译之后的文件名叫做还叫做 main.dll。
1 2 3 4 5 6 7 8 9 10 11 12 13 from ctypes import *lib = CDLL(r"./main.dll" ) try : lib.test(1 , 1.2 , b"hello world" ) except Exception as e: print (e) lib.test(c_int(1 ), c_float(1.2 ), c_char_p(b"hello world" ))
我们看到完美的打印出来了,我们再来试试布尔类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <stdio.h> void test (_Bool flag) { printf ("a = %d\n" , flag); } import ctypes from ctypes import * lib = ctypes.CDLL("./main.dll" ) lib.test(c_bool(True)) # a = 1 lib.test(c_bool(False)) # a = 0 # 可以看到 True 被解释成了 1 ,False 被解释成了 0 # 我们说整型会自动转化,而布尔类型继承自整型所以布尔类型也可以直接传递 lib.test(True) # a = 1 lib.test(False) # a = 0
然后再来看看字符和字符数组的传递:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #include <stdio.h> #include <string.h> void test (int age, char *gender) { if (age >= 18 ) { if (strcmp (gender, "female" ) == 0 ) { printf ("age >= 18, gender is female\n" ); } else { printf ("age >= 18, gender is male\n" ); } } else { if (strcmp (gender, "female" ) == 0 ) { printf ("age < 18, gender is female\n" ); } else { printf ("age < 18, gender is main\n" ); } } } from ctypes import * lib = CDLL("./main.dll" ) lib.test(c_int(20 ), c_char_p(b"female" )) # age >= 18, gender is female lib.test(c_int(20 ), c_char_p(b"male" )) # age >= 18, gender is male lib.test(c_int(14 ), c_char_p(b"female" )) # age < 18, gender is female lib.test(c_int(14 ), c_char_p(b"male" )) # age < 18, gender is main # 我们看到 C 中的字符数组,我们直接通过 c_char_p 来传递即可 # 至于单个字符,使用 c_char 即可
同理我们也可以打印宽字符,逻辑是类似的。
传递可变的字符串 我们知道 C 中不存在字符串这个概念,Python 中的字符串在 C 中也是通过字符数组来实现的,我们通过 ctypes 像 C 函数传递一个字符串的时候,在 C 中是可以被修改的。
1 2 3 4 5 6 7 8 9 10 11 #include <stdio.h> void test (char *s) { s[0 ] = 'S' ; printf ("%s" , s); } from ctypes import * lib = CDLL("./main.dll" ) lib.test(c_char_p(b"satori" )) # Satori
我们看到小写的字符串,第一个字符变成了大写,但即便能修改我们也不建议这么做,因为 bytes 对象在 Python 中是不能更改的,所以在 C 中也不应该更改。当然不是说不让修改,而是应该换一种方式。如果是需要修改的话,那么不要使用 c_char_p 的方式来传递,而是建议通过 create_string_buffer 来给 C 语言传递可以修改字符的空间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 from ctypes import *s = create_string_buffer(10 ) print (s) print (s.value) print (s.raw) print (len (s)) v = c_int(1 ) print (type (v)) print (v.value, type (v.value)) v = c_char(b"a" ) print (type (v)) print (v.value, type (v.value)) v = c_char_p(b"hello world" ) print (type (v)) print (v.value, type (v.value)) v = c_wchar_p("夏色祭" ) print (type (v)) print (v.value, type (v.value))
当然 create_string_buffer 如果只传一个 int,那么表示创建对应长度的字符缓存。除此之外,还可以指定字节串,此时的字符缓存大小和指定的字节串大小是一致的:
1 2 3 4 5 6 7 8 9 10 from ctypes import *s = create_string_buffer(b"hello" ) print (s) print (s.value) print (s.raw) print (len (s))
当然 create_string_buffer 还可以在指定字节串的同时,指定空间大小。
1 2 3 4 5 6 7 8 9 10 from ctypes import *s = create_string_buffer(b"hello" , 10 ) print (s) print (s.value) print (s.raw) print (len (s))
下面我们来看看如何使用 create_string_buffer 来传递:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <stdio.h> int test (char *s) { s[5 ] = ' ' ; s[6 ] = 's' ; s[7 ] = 'a' ; s[8 ] = 't' ; s[9 ] = 'o' ; s[10 ] = 'r' ; s[11 ] = 'i' ; printf ("s = %s\n" , s); } from ctypes import * lib = CDLL("./main.dll" ) s = create_string_buffer(b"hello" , 20 ) lib.test(s) # s = hello satori
此时就成功地修改了,我们这里的 b”hello” 占五个字节,下一个正好是索引为 5 的地方,然后把索引为 5 到 11 的部分换成对应的字符。但是需要注意的是,一定要小心 \0
,我们知道 C 语言中一旦遇到了 \0
就表示这个字符数组结束了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from ctypes import *lib = CDLL("./main.dll" ) s = create_string_buffer(b"hell" , 20 ) lib.test(s) print (s.raw) """ b'hell\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' hell的后面全部是C语言中的 \0 修改之后变成了这样 b'hell\x00 satori\x00\x00\x00\x00\x00\x00\x00\x00' 我们看到确实是把索引为5到11(包含11)的部分变成了" satori" 但是我们知道 C 语言中扫描字符数组的时候一旦遇到了 \0,就表示结束了,而hell后面就是 \0, 因为即便后面还有内容也不会输出了,所以直接就只打印了 hell """
另外除了 create_string_buffer 之外,还有一个 create_unicode_buffer,针对于 wchar_t *,用法和 create_string_buffer 类似。
调用操作系统的库函数 我们知道 Python 解释器本质上就是使用 C 语言写出来的一个软件,那么操作系统呢?操作系统本质上它也是一个软件,不管是 Windows、Linux 还是 MacOS 都自带了大量的共享库,那么我们就可以使用 Python 去调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from ctypes import *import sysimport platformsystem = platform.system() if system == "Windows" : lib = cdll.msvcrt elif system == "Linux" : lib = CDLL("libc.so.6" ) elif system == "Darwin" : lib = CDLL("libc.dylib" ) else : print ("不支持的平台,程序结束" ) sys.exit(0 ) lib.printf(b"my name is %s, age is %d\n" , b"van" , 37 ) lib.printf("姓名: %s, 年龄: %d\n" .encode("utf-8" ), "古明地觉" .encode("utf-8" ), 17 )
我们上面是在 Windows 上调用的,这段代码即便拿到 Linux 和 MacOS 上也可以正常执行。
当然这里面还支持其他的函数,我们这里以 Windows 为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from ctypes import *libc = cdll.msvcrt s = create_string_buffer(10 ) libc.strcpy(s, c_char_p(b"hello satori" )) print (s.value) s = create_unicode_buffer(10 ) libc.strcpy(s, c_wchar_p("我也觉得很变态啊" )) print (s.value) libc.puts(b"hello world" )
对于 Windows 来说,我们还可以调用一些其它的函数,但是不再是通过 cdll.msvcrt 这种方式了。在 Windows 上面有一个 user32 这么个东西,我们来看一下:
1 2 3 4 5 6 7 8 9 from ctypes import *win = cdll.user32 print (win.GetSystemMetrics(0 )) print (win.GetSystemMetrics(1 ))
我们还可以用它来打开 MessageBoxA:
可以看到我们通过 cdll.user32 就可以很轻松地调用 Windows 的 api,具体有哪些 api 可以去网上查找,搜索 win32 api 即可。
除了 ctypes,还有几个专门用来操作 win32 服务的模块,win32gui、win32con、win32api、win32com、win32process。直接 pip install pywin32 即可,或者 pip install pypiwin32。
显示窗体和隐藏窗体 1 2 3 4 5 6 7 8 9 import win32guiimport win32conqq = win32gui.FindWindow("TXGuifoundation" , "QQ" ) win32gui.ShowWindow(qq, win32con.SW_SHOW) win32gui.ShowWindow(qq, win32con.SW_HIDE)
控制窗体的位置和大小 1 2 3 4 5 6 7 8 9 10 11 12 13 14 import win32guiimport win32conqq = win32gui.FindWindow("TXGuiFoundation" , "QQ" ) win32gui.SetWindowPos(qq, win32con.HWND_TOPMOST, 100 , 100 , 300 , 300 , win32con.SWP_SHOWWINDOW)
那么我们还可以让窗体满屏幕乱跑:
1 2 3 4 5 6 7 8 9 10 11 import win32guiimport win32conimport randomqqWin = win32gui.FindWindow("TXGuiFoundation" , "QQ" ) while True : x = random.randint(1 , 1920 ) y = random.randint(1 , 1080 ) win32gui.SetWindowPos(qqWin, win32con.HWND_TOPMOST, x, y, 300 , 300 , win32con.SWP_SHOWWINDOW)
语音播放 1 2 3 4 5 import win32com.clientspeaker = win32com.client.Dispatch("SAPI.SpVoice" ) speaker.Speak("他能秒我,他能秒杀我?他要是能把我秒了,我当场······" )
Python 中 win32 模块的 api 非常多,几乎可以操作整个 Windows 提供的服务,win32 模块就是相当于把 Windows 服务封装成了一个一个的接口。不过这些服务、或者调用这些服务具体都能干些什么,可以自己去研究,这里就到此为止了。
ctypes 获取返回值 我们前面已经看到了,通过 ctypes 向动态链接库中的函数传参时是没有问题的,但是我们如何拿到返回值呢?我们之前都是使用 printf 直接打印的,但是这样显然不行,我们肯定是要拿到返回值去做一些别的事情的。那么我们在 C 函数中直接 return 不就可以啦,还记得之前演示的返回浮点型的例子吗?我们明明返回了 3.14,但得到的确是一大长串整数,所以我们需要在调用函数之前告诉 ctypes 返回值的类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int test1 (int a, int b) { int c; c = a + b; return c; } void test2 () { } from ctypes import * lib = CDLL("./main.dll" ) print(lib.test1(25 , 33 )) # 58 print(lib.test2()) # -883932787
我们看到对于 test1 的结果是正常的,但是对于 test2 来说即便返回的是 void,在 Python 中依旧会得到一个整型,因为默认都会按照整型进行解析,但这个结果肯定是不正确的。不过对于整型来说,是完全没有问题的。
正如我们传递参数一样,需要使用 ctypes 转化一下,那么在获取返回值的时候,也需要提前使用 ctypes 指定一下返回值到底是什么类型,只有这样才能拿到动态链接库中函数的正确的返回值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <wchar.h> char * test1 () { char *s = "hello satori" ; return s; } wchar_t * test2 () { wchar_t *s = L"憨八嘎" ; return s; } from ctypes import * lib = CDLL("./main.dll" ) # 不出所料,我们在动态链接库中返回的是一个字符数组的首地址,我们希望拿到指向的字符串 # 然而 Python 拿到的仍是一个整型,而且一看感觉这像是一个地址。如果是地址的话那么从理论上讲是对的,返回地址、获取地址 print(lib.test1()) # 1788100608 # 但我们希望的是获取地址指向的字符数组,所以我们需要指定一下返回的类型 # 指定为 c_char_p,告诉 ctypes 你在解析的时候将 test1 的返回值按照 c_char_p 进行解析 lib.test1.restype = c_char_p # 此时就没有问题了 print(lib.test1()) # b'hello satori' # 同理对于 unicode 也是一样的,如果不指定类型,得到的依旧是一个整型 lib.test2.restype = c_wchar_p print(lib.test2()) # 憨八嘎
因此我们就将 Python 中的类型和 C 语言中的类型通过 ctypes 关联起来了,我们传参的时候需要转化,同理获取返回值的时候也要使用 ctypes 来声明一下类型。因为默认 Python 调用动态链接库的函数返回的都是整型,至于返回的整型的值到底是什么?从哪里来的?我们不需要关心,你可以理解为地址、或者某块内存的脏数据,但是不管怎么样,结果肯定是不正确的(如果函数返回的就是整形除外)。因此我们需要提前声明一下返回值的类型。声明方式:
1 lib.CFunction.restype = ctypes类型
我们说 lib 就是 ctypes 调用 dll 或者 so 得到的动态链接库,而里面的函数就相当于是一个个的 CFunction,然后设置内部的 restype(返回值类型),就可以得到正确的返回值了。另外即便返回值设置的不对,比如:test1 返回一个 char *,但是我们将类型设置为 c_float,调用的时候也不会报错而且得到的也是一个 float,但是这个结果肯定是不对的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from ctypes import *lib = CDLL("./main.dll" ) lib.test1.restype = c_char_p print (lib.test1()) lib.test1.restype = c_float print (lib.test1()) lib.test2.restype = c_wchar_p print (lib.test2(123 , c_float(1.35 ), c_wchar_p("呼呼呼" )))
下面我们来看看浮点类型的返回值怎么获取,当然方法和上面是一样的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <math.h> float test1 (int a, int b) { float c; c = sqrt (a * a + b * b); return c; } from ctypes import * lib = CDLL("./main.dll" ) # 得到的结果是一个整型,默认都是整型。 # 我们不知道这个整型是从哪里来的,就把它理解为地址吧,但是不管咋样,结果肯定是不对的 print(lib.test1(3 , 4 )) # 1084227584 # 我们需要指定返回值的类型,告诉 ctypes 返回的是一个 float lib.test1.restype = c_float # 此时结果就是对的 print(lib.test1(3 , 4 )) # 5.0 # 如果指定为 double 呢? lib.test1.restype = c_double # 得到的结果也有问题,总之类型一定要匹配 print(lib.test1(3 , 4 )) # 5.356796015e-315 # 至于 int 就不用说了,因为默认就是 int 。所以和第一个结果是一样的 lib.test1.restype = c_int print(lib.test1(3 , 4 )) # 1084227584
所以类型一定要匹配,该是什么类型就是什么类型。即便动态链接库中返回的是 float,我们在 Python 中通过 ctypes 也要指定为 float,而不是指定为 double,尽管都是浮点数并且 double 的精度还更高,但是结果依旧不是正确的。至于整型就不需要关心了,但即便如此,int、long 也建议不要混用,而且传参的时候最好也进行转化。
ctypes 给动态链接库中的函数传递指针 我们使用 ctypes 可以创建一个字符数组并且拿到首地址,但是对于整型、浮点型我们怎么创建指针呢?下面就来揭晓。另外,一旦涉及到指针操作的时候就要小心了,因为这往往是比较危险的,所以 Python 把指针给隐藏掉了,当然不是说没有指针,肯定是有指针的。只不过操作指针的权限没有暴露给程序员,能够操作指针的只有对应的解释器。
ctypes.byref 和 ctypes.pointer 创建指针 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from ctypes import *v = c_int(123 ) print (v.value)v.value = 456 print (v) s = create_string_buffer(b"hello" ) s[3 ] = b'>' print (s.value) v2 = c_int(123 ) print (byref(v2)) print (pointer(v2))
我们看到 byref 和 pointer 都可以创建指针,那么这两者有什么区别呢?byref 返回的指针相当于右值,而 pointer 返回的指针相当于左值。举个栗子:
1 2 3 int num = 123 ;int *p = &num
对于上面的例子,如果是 byref,那么结果相当于 &num,拿到的就是一个具体的值。如果是 pointer,那么结果相当于 p。这两者在传递的时候是没有区别的,只是对于 pointer 来说,它返回的是一个左值,我们是可以继续拿来做文章的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from ctypes import *n = c_int(123 ) p1 = byref(n) p2 = pointer(n) print (byref(p2)) try : print (byref(p1)) except Exception as e: print (e)
因此两者的区别就在这里,但是还是那句话,我们在传递的时候是无所谓的,传递哪一个都可以。
传递指针 我们知道了可以通过 ctypes.byref、ctypes.pointer 的方式传递指针,但是如果函数返回的也是指针呢?我们知道除了返回 int 之外,都要指定返回值类型,那么指针如何指定呢?答案是通过 ctypes.POINTER。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 float *test1 (float *a, float *b) { static float c; c = *a + *b; return &c; } from ctypes import * lib = CDLL("./main.dll" ) # 声明一下,返回的类型是一个 POINTER(c_float),也就是 float 的指针类型 lib.test1.restype = POINTER(c_float) # 别忘了传递指针,因为函数接收的是指针,两种传递方式都可以 res = lib.test1(byref(c_float(3.14 )), pointer(c_float(5.21 ))) print(res) # <__main__.LP_c_float object at 0x000001FFF1F468C0 > print(type(res)) # <class '__main__.LP_c_float' > # 这个 res 是 ctypes 类型,和 pointer(c_float(5.21 )) 的类型是一样的,都是 <class '__main__.LP_c_float' > # 我们调用 contents 即可拿到 ctypes 中的值,那么显然在此基础上再调用 value 就能拿到 Python 中的值 print(res.contents) # c_float(8.350000381469727 ) print(res.contents.value) # 8.350000381469727
因此我们看到了如果返回的是指针类型可以使用 POINTER(类型) 来声明,也就是说 POINTER 是用来声明指针类型的,而 byref、pointer 则是用来获取指针的。
声明类型 我们知道可以事先声明返回值的类型,这样才能拿到正确的返回值。而我们传递的时候,直接传递正确的类型即可,但是其实也是可以事先声明的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from ctypes import *lib = CDLL("./main.dll" ) lib.test1.argtypes = (POINTER(c_float), POINTER(c_float)) lib.test1.restype = POINTER(c_float) try : res = lib.test1(byref(c_float(3.21 )), c_int(123 )) except Exception as e: print (e) res1 = lib.test1(byref(c_float(3.21 )), byref(c_float(666 ))) print (res1.contents.value)
传递数组 下面我们来看看如何使用 ctypes 传递数组,这里我们只讲传递,不讲返回。因为 C 语言返回数组给 Python 实际上会存在很多问题,比如:返回的数组的内存由谁来管理,不用了之后空间由谁来释放,事实上 ctypes 内部对于返回数组支持的也不是很好。因此我们一般不会向 Python 返回一个 C 语言中的数组,因为 C 语言中的数组传递给 Python 涉及到效率的问题,Python 中的列表传递直接传递一个引用即可,但是 C 语言中的数组过来肯定是要拷贝一份的,所以这里我们只讲 Python 如何通过 ctypes 给动态链接库传递数组,不再介绍动态链接库如何返回数组给 Python。
1 2 3 4 5 6 7 8 9 from ctypes import *a5 = (c_int * 5 )(1 , 2 , 3 , 4 , 5 ) print (a5) a5 = (c_int * 5 )(*range (1 , 6 )) print (a5)
下面演示一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 int test1 (int a[5 ]) { int i; int sum = 0 ; a[3 ] = 10 ; a[4 ] = 20 ; for (i = 0 ;i < 5 ; i++){ sum += a[i]; } return sum; } from ctypes import * lib = CDLL("./main.dll" ) # 创建 5 个元素的数组,但是只给3 个元素 arr = (c_int * 5 )(1 , 2 , 3 ) # 在动态链接库中,设置剩余两个元素 # 所以如果没问题的话,结果应该是 1 + 2 + 3 + 10 + 20 print(lib.test1(arr)) # 36
传递结构体 有了前面的数据结构还不够,我们还要看看结构体是如何传递的,有了结构体的传递,我们就能发挥更强大的功能。那么我们来看看如何使用 ctypes 定义一个结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from ctypes import *""" struct Girl { char *name; // 姓名 int age; // 年龄 char *gender; //性别 int class; //班级 }; """ class Girl (Structure ): _fields_ = [ ("name" , c_char_p), ("age" , c_int), ("gender" , c_char_p), ("class" , c_int) ]
我们向 C 中传递一个结构体,然后再返回:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 struct Girl { char *name; int age; char *gender; int class ; }; struct Girl test1 (struct Girl g) { g.name = "古明地觉" ; g.age = 17 ; g.gender = "female" ; g.class = 2 ; return g; } from ctypes import * lib = CDLL("./main.dll" ) class Girl(Structure): _fields_ = [ ("name" , c_char_p), ("age" , c_int), ("gender" , c_char_p), ("class" , c_int) ] # 此时返回值类型就是一个 Girl 类型,另外我们这里的类型和 C 中结构体的名字不一样也是可以的 lib.test1.restype = Girl # 传入一个实例,拿到返回值 g = Girl() res = lib.test1(g) print(res, type(res)) # <__main__.Girl object at 0x0000015423A06840 > <class '__main__.Girl' > print(res.name, str(res.name, encoding="utf-8" )) # b'\xe5\x8f\xa4\xe6\x98\x8e\xe5\x9c\xb0\xe8\xa7\x89' 古明地觉 print(res.age) # 17 print(res.gender) # b'female' print(getattr(res, "class" )) # 2
如果是结构体指针呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 struct Girl { char *name; int age; char *gender; int class ; }; struct Girl *test1 (struct Girl *g) { g -> name = "mashiro" ; g -> age = 17 ; g -> gender = "female" ; g -> class = 2 ; return g; } from ctypes import * lib = CDLL("./main.dll" ) class Girl(Structure): _fields_ = [ ("name" , c_char_p), ("age" , c_int), ("gender" , c_char_p), ("class" , c_int) ] # 此时指定为 Girl 类型的指针 lib.test1.restype = POINTER(Girl) # 传入一个实例,拿到返回值 # 但返回的是指针,我们还需要手动调用一个 contents 才可以拿到对应的值。 g = Girl() res = lib.test1(byref(g)) print(str(res.contents.name, encoding="utf-8" )) # mashiro print(res.contents.age) # 16 print(res.contents.gender) # b'female' print(getattr(res.contents, "class" )) # 3 # 另外我们不仅可以通过返回的 res 去调用,还可以通过 g 来调用,因为我们传递的是 g 的指针 # 修改指针指向的内存就相当于修改g,所以我们通过g来调用也是可以的 print(str(g.name, encoding="utf-8" )) # mashiro
因此对于结构体来说,我们先创建一个结构体(Girl)实例 g,如果动态链接库的函数中接收的是结构体,那么直接把 g 传进去等价于将 g 拷贝了一份,此时函数中进行任何修改都不会影响原来的 g。但如果函数中接收的是结构体指针,我们传入 byref(g) 相当于把 g 的指针拷贝了一份,在函数中修改是会影响 g 的。而返回的 res 也是一个指针,所以我们除了通过 res.contents 来获取结构体中的值之外,还可以通过 g 来获取。再举个栗子对比一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 struct Num { int x; int y; }; struct Num test1(struct Num n){ n.x += 1 ; n.y += 1 ; return n; } struct Num *test2(struct Num *n){ n->x += 1 ; n->y += 1 ; return n; } from ctypes import *lib = CDLL("./main.dll" ) class Num (Structure ): _fields_ = [ ("x" , c_int), ("y" , c_int), ] num = Num(x=1 , y=2 ) print (num.x, num.y) lib.test1.restype = Num res = lib.test1(num) print (res.x, res.y) print (num.x, num.y) """ 因为我们将 num 传进去之后,相当于将 num 拷贝了一份。 函数里面的结构体和这里的 num 尽管长得一样,但是没有任何关系 所以 res 获取的结果是自增之后的结果,但是 num 还是之前的 num """ lib.test2.restype = POINTER(Num) res = lib.test2(byref(num)) print (num.x, num.y) """ 我们看到将指针传进去之后,相当于把 num 的指针拷贝了一份。 然后在函数中修改,相当于修改指针指向的内存,所以是会影响外面的 num 的 而动态链接库的函数中返回的是参数中的结构体指针,而我们传递的 byref(num) 也是这里的num的指针 尽管传递指针的时候也是拷贝了一份,两个指针本身来说虽然也没有任何联系,但是它们存储的地址是一样的 那么通过 res.contents 获取到的内容就相当于是这里的 num 因此此时我们通过 res.contents 获取和通过 num 来获取都是一样的。 """ print (res.contents.x, res.contents.y)
所以在这里,C 中返回一个指针是没有问题的,因为它指向的对象是我们在 Python 中创建的,Python 会管理它。
回调函数 在看回调函数之前,我们先看看如何把一个函数赋值给一个变量。准确的说,是让一个指针指向一个函数,这个指针叫做函数指针。通常我们说的指针变量是指向一个整型、字符型或数组等等,而函数指针是指向函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <stdio.h> int add (int a, int b) { int c; c = a + b; return c; } int main () { int (*p)(int , int ) = add; printf ("1 + 3 = %d\n" , p(1 , 3 )); return 0 ; }
除此之外我们还以使用 typedef。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <stdio.h> int add (int a, int b) { int c; c = a + b; return c; } typedef int (*func) (int , int ) ;int main () { func p = add; printf ("2 + 3 = %d\n" , p(2 , 3 )); return 0 ; }
下面来看看如何使用回调函数,说白了就是把一个函数指针作为函数的参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include <stdio.h> char *evaluate (int score) { if (score < 60 && score >= 0 ){ return "bad" ; }else if (score < 80 ){ return "not bad" ; }else if (score < 90 ){ return "good" ; }else if (score <=100 ){ return "excellent" ; }else { return "无效的成绩" ; } } char *execute1 (int score, char *(*f)(int )) { return f(score); } typedef char *(*func)(int );char *execute2 (int score, func f) { return f(score); } int main (int argc, char const *argv[]) { printf ("%s\n" , execute1(88 , evaluate)); printf ("%s\n" , execute2(70 , evaluate)); }
我们知道了在 C 中传入一个函数,那么在 Python 中如何定义一个 C 语言可以识别的函数呢?毫无疑问,类似于结构体,我们肯定是要先定义一个 Python 的函数,然后再把 Python 的函数转化成 C 语言可以识别的函数。
1 2 3 int add (int a, int b, int (*f)(int *, int *)) { return f(&a, &b); }
我们就以这个函数为例,add 函数返回一个 int,接收两个 int,和一个函数指针,那么我们如何在 Python 中定义这样的函数并传递呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 from ctypes import *lib = CDLL("./main.dll" ) def add (a, b ): return a.contents.value + b.contents.value t = CFUNCTYPE(c_int, POINTER(c_int), POINTER(c_int)) func = t(add) lib.add.restype = c_int print (lib.add(88 , 96 , func))print (lib.add(59 , 55 , func))print (lib.add(94 , 105 , func))""" 184 114 199 """
以上便是 ctypes 的基本用法,但其实我们可以通过 ctypes 玩出更高级的花样,甚至可以串改内部的解释器。ctypes 内部提供了一个属性叫 pythonapi,它实际上就是加载了 Python 安装目录里面的 python38.dll。有兴趣可以自己去了解一下,需要你了解底层的 Python / C API,当然我们也很少这么做。对于 ctypes 调用 C 库而言,我们目前算是介绍完了。
使用 C / C++ 为 Python 开发扩展模块 我们上面介绍 ctypes,我们说这种方式它不涉及任何的 Python / C API,但是它只能做一些简单的交互。而如果是编写扩展模块的话,那么它是可以被 Python 解释器识别的,也就是说我们可以通过 import 的方式进行导入。
关于扩展模块,这里不得不再提一下 Cython,使用 Python / C API 编写扩展不是一件轻松的事情,其实还是 C 语言本身比较底层吧。而 Cython 则是帮我们解决了这一点,Cython 代码和 Python 高度相似,而 cython 编译器会自动帮助我们将 Cython 代码翻译成C代码,所以Cython本质上也是使用了 Python / C API。只不过它让我们不需要直接面对C,只要我们编写 Cython 代码即可,会自动帮我们转成 C 的代码。
所以随着 Cython 的出现,现在使用 Python / C API 编写扩展算是越来越少了,不过话虽如此,使用 Python / C API 编写可以极大的帮助我们熟悉 Python 的底层。
那么废话不多说,直接开始吧。
编写扩展模块的基本骨架 首先使用 C / C++ 为 Python 编写扩展的话,是需要遵循一定套路的,而这个套路很固定。那么下面就来介绍一下整个流程:
Python 的扩展模块是需要被 import 进来的,那么它必然要有一个入口。
有了入口之后,我们还需要创建模块,创建模块使用下面这个函数。
创建模块,那么总要有模块信息吧。
那么模块信息里面都可以包含哪些信息呢?模块名算吧,模块里面有哪些函数算吧。
而一个 Python 中的函数底层会对应一个结构体,这个结构体里面保存了 Python 函数的元信息,并且还保存了一个指向 C 函数的指针,这是显然的。
我们通过一个例子来说明以下吧,这样会更好理解一些,具体细节在编写代码的时候再补充。
1 2 3 4 5 6 def f1 (): return 123 def f2 (a ): return a + 1
以上是非常简单的一个模块,里面只有两个简单的函数,但是我们知道当被导入时它就是一个 PyModuleObject 对象。里面除了我们定义的两个函数之外还有其它的属性,显然这是 Python 解释器在背后帮助我们完成的,具体流程也是我们上面说的那几步(省略了亿点点细节)。
那么我们如何使用 C 来进行编写呢?下面来操作一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 #include "Python.h" static PyObject *f1 (PyObject *self) { return PyLong_FromLong(123 ); } static PyObject *f2 (PyObject *self, PyObject *a) { long x; x = PyLong_AsLong(a); PyObject *result = PyLong_FromLong(x + 1 ); return result; } static PyMethodDef methods[] = { { "f1" , (PyCFunction) f1, METH_NOARGS, "this is a function named f1" }, {"f2" , (PyCFunction) f2, METH_O, "this is a function named f2" }, {NULL , NULL , 0 , NULL } }; static PyModuleDef module = { PyModuleDef_HEAD_INIT, "kagura_nana" , "this is a module named kagura_nana" , -1 , methods, NULL , NULL , NULL , NULL }; PyMODINIT_FUNC PyInit_kagura_nana(void ) { return PyModule_Create(&module); }
整体逻辑还是非常简单的,过程如下:
include "Python.h",这个是必须的
定义我们函数,具体定义什么函数、里面写什么代码完全取决于你的业务
定义一个PyMethodDef结构体数组
定义一个PyModuleDef结构体
定义模块初始化入口,然后返回模块对象
那么如何将这个 C 文件变成扩展模块呢?显然要经过编译,而 Python 提供了 distutils 标准库,可以非常轻松地帮我们把 C 文件编译成扩展模块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from distutils.core import *setup( name="kagura_nana" , version="1.11" , author="古明地盆" , author_email="66666@东方地灵殿.com" , ext_modules=[Extension("kagura_nana" , ["main.c" ])] )
当前的 py 文件名叫做 1.py,我们在控制台中直接输入 python 1.py install 即可。注意:在介绍 ctypes 我用的是 gcc,但这里默认是使用 Visual Studio 2017 进行编译的。
我们看到对应的 pyd 已经生成了,在你当前目录会有一个 build目录,然后 build 目录中 lib 开头的目录里面便存放了编译好的 pyd文件,并且还自动帮我们拷贝到了 site-packages 目录中。
我们看到了 kagura_nana.cp38-win_amd64.pyd 文件,中间的部分表示解释器的版本,所以编写扩展模块的方式虽然可定制性更高,但它除了操作系统之外,还需要特定的解释器版本。因为中间是 cp38,所以只能 Python3.8 版本的解释器才可以导入它。然后还有一个 egg-info,它是我们编写的模块的元信息,我们打开看看。
有几个我们没有写,所以是 UNKNOW,当然这都不重要,重要的是我们能不能调用,试一试吧。
1 2 3 4 5 import kagura_nanaprint (kagura_nana) print (kagura_nana.f1()) print (kagura_nana.f2(123 ))
可以看到调用是没有任何问题的,最后再看一个神奇的东西,我们知道在 pycharm 这样的智能编辑器中,通过 Ctrl 加左键可以调到指定模块的指定位置。
神奇的一幕出现了,我们点击进去居然还能跳转,其实我们在编译成扩展模块移动到 site-packages
之后,pycharm 会进行检测、然后将其抽象成一个普通的 py 文件,方便你查看。我们看到模块注释、函数的注释跟我们在 C 文件中指定的一样。但是注意:该文件只是 pycharm 方便你查看函数注释等信息而专门做的一个抽象,事实上你把这个文件删掉也是没有关系的。
因此我们可以再总结一下整体流程:
第一步:include “Python.h”,必须要引入这个头文件,这个头文件中还引入了 C 中的一些头文件,具体都引入了哪些库我们可以查阅。当然如果不确定但又懒得看,我们还可以手动再引入一次,反正 include 同一个头文件只会引入一次。
第二步:理论上这不是第二步,但是按照编写代码顺序我们就认为它是第二步吧,对,就是按照我们上面写的代码从上往下撸。这一步你需要编写函数,这个函数就是 C 语言中定义的函数,这个函数返回一个 PyObject * ,至少要接收一个PyObject *,我们一般叫它 self,这第一个参数你可以看成是必须的,无论我们传不传其他参数,这个参数是必需要有的。所以如果只有这一个参数,那么我们就认为这个函数不接收参数,因为我们在调用的时候没有传递。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 static PyObject *f1 (PyObject *self) { } static PyObject *f2 (PyObject *self) { } static PyObject *f3 (PyObject *self) { }
第三步:定义一个 PyMethodDef 类型的数组,这个数组也是我们后面的 PyModuleDef 对象中的一个参数,这个数组名字叫什么就无所谓了。至于 PyMethodDef,我们可以单独使用 PyMethodDef 创建实例,然后将变量写到数组中,也可以直接在数组中创建。如果是直接在数组中创建的话,那么就不需要再使用 PyMethodDef 定义了,直接在 {} 里面写成员信息即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static PyMethodDef module_functions[] = { { "f1" , (PyCFunction) f1, METH_NOARGS, "函数f1的注释" }, {"f2" , (PyCFunction)f2, METH_NOARGS, "函数f2的注释" }, {"f3" , (PyCFunction)f3, METH_NOARGS, "函数f3的注释" }, {NULL , NULL , 0 , NULL } }
第四步:定义 PyModuleDef 对象,这个变量的名字叫什么也没有要求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 static PyModuleDef m = { PyModuleDef_HEAD_INIT, "kagura_nana" , "模块的注释" , -1 , module_functions, NULL , NULL , NULL , NULL }
第五步:写上一个宏,其实把它单独拆分出来,有点小题大做了。
第六步:创建一个模块的入口函数,我们说编译的扩展模块叫 kagura_nana,那么这个函数名就要这么写。
1 2 3 4 5 6 7 PyInit_kagura_nana(void ) { return PyModule_Create(&m); }
第七步:定义一个py文件,假设叫 xx.py,那么在里面写上如下内容,然后 python xx.py install 即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from distutils.core import *setup( name="kagura_nana" , version="10.22" , author="古明地觉" , author_email="东方地灵殿" , ext_modules=[Extension("hanser" , ["a.c" ])]
以上便是编写扩展模块的基本流程,但是里面还有很多细节没有说。
PyMethodDef 首先是 PyMethodDef,我们说它对应的是 Python 中的函数,那么我们肯定要来看看它的定义,藏身于 *Include/methodobject.h* 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 struct PyMethodDef { const char *ml_name; PyCFunction ml_meth; int ml_flags; const char *ml_doc; }; typedef struct PyMethodDef PyMethodDef ;
如果不需要参数,那么 ml_flags 传入一个 METH_NOARGS;接收一个参数传入 METH_O;所以我们上面的 f1 对应的 ml_flags 是 METHOD_NOARGS,f2 对应的 ml_flags 是 METH_O。
如果是多个参数,那么直接写成 METH_VARAGRS 即可,也就是通过扩展位置参数的方式,但是这要如何解析呢?比如:有一个函数f3接收3个参数,这在C中要如何实现呢?别急我们后面会说。
引用计数和内存管理 我们在最开始的时候就说过,PyObject 贯穿了我们的始终。我们说这里面存放了引用计数和类型指针,并且 Python 中所有对象底层对应的结构体都嵌套了 PyObject,因此 Python 中的所有对象都有引用计数和类型。并且 Python 的对象在底层,都可以看成是 PyObject 的一个扩展,因此参数、返回值都是 PyObject *,至于具体类型则是通过里面的 ob_type 动态判断。比如:之前使用的 PyLong_FromLong。
1 2 3 4 5 6 7 PyObject * PyLong_FromLong (long ival) { PyLongObject *v; return (PyObject *)v; }
此外 Python 还专门定义了几个宏,来看一下:
1 2 3 #define Py_REFCNT(ob) (((PyObject*)(ob))->ob_refcnt) #define Py_TYPE(ob) (((PyObject*)(ob))->ob_type) #define Py_SIZE(ob) (((PyVarObject*)(ob))->ob_size)
Py_REFCNT:拿到对象的引用计数;Py_TYPE:拿到对象的类型;Py_SIZE:拿到对象的ob_size,也就是变长对象里面的元素个数。除此之外,Python 还提供了两个宏:Py_INCREF 和 Py_DECREF 来用于引用计数的增加和减少。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #define Py_INCREF(op) ( \ _Py_INC_REFTOTAL _Py_REF_DEBUG_COMMA \ ((PyObject *)(op))->ob_refcnt++) #define Py_DECREF(op) \ do { \ PyObject *_py_decref_tmp = (PyObject *)(op); \ if (_Py_DEC_REFTOTAL _Py_REF_DEBUG_COMMA \ --(_py_decref_tmp)->ob_refcnt != 0) \ _Py_CHECK_REFCNT(_py_decref_tmp) \ else \ _Py_Dealloc(_py_decref_tmp); \ } while (0)
当然这些东西我们在系列的最开始的时候就已经说过了,但是接下来我们要引出一个非常关键的地方,就是内存管理。到目前为止我们没有涉及到内存管理的操作,但我们知道 Python 中的对象都是申请在堆区的,这个是不会自动释放的。举个栗子:
1 2 3 4 5 6 7 8 9 static PyObject *f (PyObject *self) { PyObject *s = PyUnicode_FromString("你好呀~~~" ); Py_INCREF(Py_None); return Py_None; }
这个函数不需要参数,如果我们写一个死循环不停的调用这个函数,你会发现内存的占用蹭蹭的往上涨。就是因为这个 PyUnicodeObject 是申请在堆区的,此时内部的引用计数为 1。函数执行完毕变量 s 被销毁了,但是 s 是一个指针,这个指针被销毁了是不假,但是它指向的内存并没有被销毁。
1 2 3 4 5 6 7 8 static PyObject *f (PyObject *self, PyObject *args, PyObject *kw) { PyObject *s = PyUnicode_FromString("hello~~~" ); Py_DECREF(s); Py_INCREF(Py_None); return Py_None; }
因此我们需要手动调用 Py_DECREF 这个宏,来将 s 指向的 PyUnicodeObject 的引用计数减 1,这样引用计数就为 0 了。不过有一个特例,那就是当这个指针作为返回值的时候,我们不需要手动减去引用计数,因为会自动减。
1 2 3 4 5 6 7 8 9 10 11 12 static PyObject *f (PyObject *self) { PyObject *s = PyUnicode_FromString("hello~~~" ); return s; }
不过这里还存在一个问题,那就是我们在 C 中返回的是 Python 传过来的。
1 2 3 4 5 6 static PyObject *f (PyObject *self, PyObject *val) { return val; }
显然上面 val 指向的内存不是在 C 中调用 api 创建的,而是 Python 创建然后传递过来的,也就是说这个 val 已经指向了一块合法的内存(和增加 Py_None 引用计数类似)。但是内存中的对象的引用计数是没有变化的,虽说有新的变量(这里的 val)指向它了,但是这个 val 是 C 中的变量不是 Python 中的变量,因此它的引用计数是没有变化的。然后作为返回值返回之后,指向对象的引用计数减一。所以你会发现在 Python 中,创建一个变量,然后传递到 f 中,执行完之后再进行打印就会发生段错误,因为对应的内存已经被回收了。如果能正常打印,说明在 Python 中这个变量的引用计数不为 1,也可能是小整数对象池、或者有多个变量引用,那么就创建一个大整数或者其他的对象多调用几次,因为作为返回值,每次调用引用计数都会减1。
1 2 3 4 5 6 7 8 9 10 static PyObject *f (PyObject *self) { PyObject *l1 = PyList_New(2 ); PyObject *l2 = l1; Py_INCREF(Py_None); return Py_None; }
因此我们说,如果在 C 中创建一个 PyObject 的话,那么它的引用计数会是 1,因为对象被初始化了,引用计数默认是 1。至于传递,无论你在 C 中将创建 PyObject * 赋值给了多少个变量,它们指向的 PyObject 的引用计数都会是 1。因为这些变量是 C 中的变量,不是 Python 中的。
因此我们的问题就很好解释了,我们说当一个 PyObject * 作为返回值的时候,它指向的对象的引用计数会减去 1,那么当 Python 传递过来一个 PyObject * 指针的时候,由于它作为了返回值,因此调用之后会发现引用计数会减少了。因此当你在 Python 中调用扩展函数结束之后,这个变量指向的内存可能就被销毁了。如果你在 Python 传递过来的指针没有作为返回值,那么引用计数是不会发生变化的,但是一旦作为了返回值,引用计数会自动减 1,因此我们需要手动的加 1。
1 2 3 4 5 6 static PyObject *f (PyObject *self, PyObject *val) { Py_INCREF(val); return val; }
因此我们可以得出如下结论:
如果在 C 中,创建一个 PyObject *var,并且 var 已经指向了合法的内存,比如调用 PyList_New、PyDict_New 等等 api 返回的 PyObject *,总之就是已经存在了 PyObject。那么如果 var 没有作为返回值,我们必须手动地将 var 指向的对象的引用计数减 1,否则这个对象就会在堆区一直待着不会被回收。可能有人问,如果 PyObject *var2 = var,我将 var 再赋值给一个变量呢?那么只需要对一个变量进行 Py_DECREF 即可,当然对哪个变量都是一样的,因为在 C 中变量的传递不会导致引用计数的增加。
如果 C 中创建的 PyObject * 作为返回值了,那么会自动将指向的对象的引用计数减 1,因此此时该指针指向的内存就由 Python 来管理了,就相当于在 Python 中创建了一个对象,我们不需要关心。
最后关键的一点,如果 C 中返回的指针指向的内存是 Python 中创建好的,假设我们在 Python 中创建了一个对象,然后把指针传递过来了,但是我们说这不会导致引用计数的增加,因为赋值的变量是 C 中的变量。如果 C 中用来接收参数的指针没有作为返回值,那么引用计数在扩展函数调用之前是多少、调用之后还是多少。然而一旦作为了返回值,我们说引用计数会自动减 1,因此假设你在调用扩展函数之前引用计数是 3,那么调用之后你会发现引用计数变成了2。为了防止段错误,一旦作为返回值,我们需要在返回之前手动地将引用计数加1。
C中创建的:不作为返回值,引用计数手动减 1、作为返回值,不处理;Python 中创建传递过来的,不作为返回值,不处理、作为返回值,引用计数手动加 1。
而实现引用计数增加和减少所使用的宏就是 Py_INCREF 和 Py_DECREF,但它们要求传递的 PyObject * 不可以为 NULL。如果可能为 NULL 的话,那么建议使用 Py_XINCREF 和 Py_XDECREF。
参数的解析 我们说,PyMethodDef 内部有一个 ml_flags 属性,表示此函数的参数类型,我们说有如下几种:
1. 不接受参数,METH_NOARGS,对应函数格式如下:
1 2 3 4 5 static PyObject *f (PyObject *self) { }
2. 接受一个参数,METH_O,对应函数格式如下:
1 2 3 4 5 static PyObject * f(PyObject *self, PyObject *val) { }
3. 接受任意个位置参数,METH_VARARGS,对应函数格式如下:
1 2 3 4 5 static PyObject *f (PyObject *self, PyObject *args) { }
4. 接受任意个位置参数和关键字参数,METH_VARARGS | METH_KEYWORDS,对应函数格式如下:
1 2 3 4 5 static PyObject * f(PyObject *self, PyObject *args, PyObject *kwargs) { }
第一种和第二种显然都很简单,关键是第三种和第四种要怎么做呢?我们先来看看第三种,解析多个位置参数可以使用一个函数:PyArg_ParseTuple。
解析多个位置参数 1 函数原型:int PyArg_ParseTuple(PyObject *args, const char *format, ...); 位于 Python/getargs.c 中
所以重点就在 PyArg_ParseTuple 上面,我们注意到里面有一个 format,显然类似于 printf,里面肯定是一些占位符,那么都支持哪些占位符呢?常用的如下:
i:接收一个 Python 中的 int,然后解析成 C 的 int
l:接收一个 Python 中的 int,然后将传来的值解析成 C 的 long
f:接收一个 Python 中的 float,然后将传来的值解析成 C 的 float
d:接收一个 Python 中的 float,然后将传来的值解析成 C 的 double
s:接收一个 Python 中的 str,然后将传来的值解析成 C 的 char *
u:接收一个 Python 中的 str,然后将传来的值解析成 C 的 wchar_t *
O:接收一个 Python 中的 object,然后将传来的值解析成 C 的 PyObject *
我们举个栗子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 static PyObject *f (PyObject *self, PyObject *args) { int a, b, c; if (!PyArg_ParseTuple(args, "iii" , &a, &b, &c)){ return NULL ; } return PyLong_FromLong(a + b + c); }
我们还是编译一下,当然编译的过程我们就不显示了,跟之前是一样的。并且为了方便,我们的模块名就不改了,但是编译之后的 pyd 文件内容已经变了。不过需要注意的是,我们说编译之后会有一个 build 目录,然后会自动把里面的 pyd 文件拷贝到 site-packages 中,如果你修改了代码,但是模块名没有变的话,那么编译之后的文件名还和原来一样。如果一样的话,那么由于已经存在相同文件了,可能就不会再拷贝了。因此两种做法:要么你把模块名给改了,这样编译会生成新的模块。要么编译之前记得把上一次编译生成的 build 目录先删掉,我们推荐第二种做法,不然 site-packages 目录下会出现一大堆我们自己定义的模块。
然后我们将 ml_flags 改成 METH_VARARGS,来测试一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #include "Python.h" static PyObject *f (PyObject *self, PyObject *args) { int a, b, c; if (!PyArg_ParseTuple(args, "iii" , &a, &b, &c)){ return NULL ; } return PyLong_FromLong(a + b + c); } static PyMethodDef methods[] = { { "f" , (PyCFunction) f, METH_VARARGS, "this is a function named f" }, {NULL , NULL , 0 , NULL } }; static PyModuleDef module = { PyModuleDef_HEAD_INIT, "kagura_nana" , "this is a module named kagura_nana" , -1 , methods, NULL , NULL , NULL , NULL }; PyMODINIT_FUNC PyInit_kagura_nana (void ) { return PyModule_Create(&module); }
我们编译成扩展模块之后,来测试一下,但是注意,你在调用的时候 pycharm 可能会感到别扭。
因为在调用函数 f 的是给你飘黄了,原因就是我们上一次在生成 pyd 的时候,里面的函数是 f1 和 f2,并没有 f。而我们 pycharm 会将 pyd 抽象成一个普通的 py 文件让你查看,但同时它也是 pycharm 自动提示的依据。因为上一次 pycharm 已经抽象出来了这个文件,而里面没有 f 这个函数,所以这里会飘黄。但是不用管,因为我们调用的是生成的 pyd 文件,跟 pycharm 抽象出来的 py 文件无关。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import kagura_nanatry : print (kagura_nana.f()) except TypeError as e: print (e) try : print (kagura_nana.f(123 )) except TypeError as e: print (e) try : print (kagura_nana.f(123 , "xxx" , 123 , 123 )) except TypeError as e: print (e) try : kagura_nana.f(123 , 123.0 , 123 ) except TypeError as e: print (e) print (kagura_nana.f(123 , 123 , 123 ))
怎么样,是不是很简单呢?当然 PyArg_ParseTuple 解析失败,Python 底层自动帮你报错了,告诉你缺了几个参数,或者哪个参数的类型错了。
我们这里是以 i 进行演示的,至于其它的几个占位符也是类似的。当然 O 比较特殊,因为它是转成 PyObject *,所以此时我们是可以传递元组、列表、字典等任意高阶对象的。而我们之前的 ctypes 则是不支持的,还是那句话,因为它没有涉及任何 Python / C API 的调用,显然数据的表达能力有限。
解析成 PyObject * 我们说 PyArg_ParseTuple 中的 i 代表 int、l 代表 long、f 代表 float、d 代表 double、s 代表 char*、u代表 wchar_t *,这些都比较简单。我们重点是 O,其实 O 也不难,无非就是后续的一些 Python / C API 调用罢了。
我们还是以普通的 py 文件为例:
1 2 3 4 5 6 7 8 9 def foo (lst: list ): """ 假设我们传递一个列表, 然后返回一个元组, 并且将里面的元素都设置成元素的类型 :return: """ return tuple ([type (item) for item in lst]) print (foo([1 , 2 , "3" , {}]))
如果使用 C 来编写扩展的话,要怎么做呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #include "Python.h" static PyObject *foo (PyObject *self, PyObject *args) { PyObject *lst; if (!PyArg_ParseTuple(args, "O" , &lst)){ return NULL ; } Py_ssize_t arg_count = Py_SIZE(lst); PyObject *tpl = PyTuple_New(arg_count + 1 ); PyObject *type, *val; for (int i = 0 ; i < arg_count; i++) { val = PyList_GetItem(lst, i); type = (PyObject *)val -> ob_type; PyTuple_SetItem(tpl, i, type); } return tpl; } static PyMethodDef methods[] = { { "foo" , (PyCFunction) foo, METH_VARARGS, NULL }, {NULL , NULL , 0 , NULL } }; static PyModuleDef module = { PyModuleDef_HEAD_INIT, "kagura_nana" , "this is a module named kagura_nana" , -1 , methods, NULL , NULL , NULL , NULL }; PyMODINIT_FUNC PyInit_kagura_nana (void ) { return PyModule_Create(&module); }
然后使用 Python 测试一下:
1 2 3 4 5 6 7 8 9 10 import kagura_nanaprint ( kagura_nana.foo([1 , 2 , "3" , {}]) )
从这里我们也能看出使用 C 来为 Python 写扩展是一件多么麻烦的事情,因此 Cython 的出现是一个福音。当然我们上面的代码只是演示,没有太大意义,完全可以用 Python 实现。
传递字符串 然后我们再来看看字符串的传递,比较简单,说白了这些都是 Python / C API 的调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 #include "Python.h" static PyObject *f1 (PyObject *self, PyObject *args) { Py_ssize_t arg_count = Py_SIZE(args); PyObject *res = PyUnicode_FromWideChar(L"" , 0 ); for (int i=0 ; i < arg_count; i++){ res = PyUnicode_Concat(res, PyTuple_GetItem(args, i)); } PyObject *lst = PyList_New(1 ); PyList_SetItem(lst, 0 , res); return lst; } static PyObject *f2 (PyObject *self, PyObject *args) { PyObject *res = PyUnicode_Join(PyUnicode_FromWideChar(L"||" , 2 ), args); return res; } static PyMethodDef methods[] = { { "f1" , (PyCFunction) f1, METH_VARARGS, NULL }, { "f2" , (PyCFunction) f2, METH_VARARGS, NULL }, {NULL , NULL , 0 , NULL } }; static PyModuleDef module = { PyModuleDef_HEAD_INIT, "kagura_nana" , "this is a module named kagura_nana" , -1 , methods, NULL , NULL , NULL , NULL }; PyMODINIT_FUNC PyInit_kagura_nana (void ) { return PyModule_Create(&module); }
Python 进行调用,看看结果。
1 2 3 4 import kagura_nanaprint (kagura_nana.f1("哼哼" , "嘿嘿" , "哈哈" )) print (kagura_nana.f2("哼哼" , "嘿嘿" , "哈哈" ))
我们看到结果是没有问题的,还是蛮有趣的。
类型检查和返回异常 在 Python 中,当我们传递的类型不对时会报错。那么在底层我如何才能检测传递过来的参数是不是想要的类型呢?首先我们想到的是通过 ob_type,假设我们要求 val 是一个 int,那么:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #include "Python.h" static PyObject *f1 (PyObject *self, PyObject *val) { const char *tp_name = val -> ob_type -> tp_name; char *res; if (strcmp (tp_name, "int" ) == 0 ) { res = "success" ; } else { res = "failure" ; } return PyUnicode_FromString(res); } static PyMethodDef methods[] = { { "f1" , (PyCFunction) f1, METH_O, NULL }, {NULL , NULL , 0 , NULL } }; static PyModuleDef module = { PyModuleDef_HEAD_INIT, "kagura_nana" , "this is a module named kagura_nana" , -1 , methods, NULL , NULL , NULL , NULL }; PyMODINIT_FUNC PyInit_kagura_nana (void ) { return PyModule_Create(&module); } import kagura_nana print (kagura_nana.f1(123 )) # success print (kagura_nana.f1("123" )) # failure
以上是一种判断方式,但是 Python 底层给我们提供了其它的 API 来进行判断。比如:
判断是否为整型: PyLong_Check
判断是否为字符串: PyUnicode_Check
判断是否为浮点型: PyFloat_Check
判断是否为复数: PyComplex_Check
判断是否为元组: PyTuple_Check
判断是否为列表: PyList_Check
判断是否为字典: PyDict_Check
判断是否为集合: PySet_Check
判断是否为字节串: PyBytes_Check
判断是否为函数: PyFunction_Check
判断是否为方法: PyMethod_Check
判断是否为实例对象: PyInstance_Check
判断是否为类(type的实例对象): PyType_Check
判断是否为可迭代对象: PyIter_Check
判断是否为数值: PyNumber_Check
判断是否为序列(实现 __getitem__ 和 __len__): PySequence_Check
判断是否为映射(必须实现 __getitem__、__len__ 和 __iter__): PyMapping_Check
判断是否为模块: PyModule_Check
写法非常固定,因此我们上面的判断逻辑就可以进行如下修改:
1 2 3 4 5 6 7 8 9 10 11 static PyObject *f1 (PyObject *self, PyObject *val) { char *res; if (PyLong_Check(val)) { res = "success" ; } else { res = "failure" ; } return PyUnicode_FromString(res); }
这种写法是不是就简单多了呢?其它部分不需要动,然后你可以自己重新编译、并测试一下,看看结果是不是一样的。
然后问题来了,如果用户传递的参数个数不对,或者类型不对,那么我们应该返回一个 TypeError,或者说返回一个异常。那么在 C 中,要如何设置异常呢?其实设置异常,说白了就是把输出信息打印到 stderr 中,然后直接返回 NULL 即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 #include "Python.h" static PyObject *f1 (PyObject *self, PyObject *args) { Py_ssize_t arg_count = Py_SIZE(args); if (arg_count != 3 ) { PyErr_Format(PyExc_TypeError, ">>>>>> f1() takes 3 positional arguments but %d were given" , arg_count); } PyObject *a, *b, *c; PyArg_ParseTuple(args, "OOO" , &a, &b, &c); if (!PyLong_Check(a)) { PyErr_Format(PyExc_ValueError, "The 1th argument requires a int, but got %s" , Py_TYPE(a) -> tp_name); } if (!PyUnicode_Check(b)) { PyErr_Format(PyExc_ValueError, "The 2th argument requires a str, but got %s" , Py_TYPE(b) -> tp_name); } if (!PyList_Check(c)) { PyErr_Format(PyExc_ValueError, "The 3th argument requires a list, but got %s" , Py_TYPE(c) -> tp_name); } PyList_Append(c, a); PyList_Append(c, b); Py_INCREF(c); return c; } static PyMethodDef methods[] = { { "f1" , (PyCFunction) f1, METH_VARARGS, NULL }, {NULL , NULL , 0 , NULL } }; static PyModuleDef module = { PyModuleDef_HEAD_INIT, "kagura_nana" , "this is a module named kagura_nana" , -1 , methods, NULL , NULL , NULL , NULL }; PyMODINIT_FUNC PyInit_kagura_nana (void ) { return PyModule_Create(&module); }
所以逻辑就是像上面那样,通过 PyErr_Format 来设置异常,这个会被 Python 端接收到,但是异常一旦设置,就必须要返回 NULL,否则会出现段错误。但反过来吗,返回 NULL 的话则不一定要设置异常,但如果你不设置,那么 Python 底层会默认帮你设置一个 SystemError,并且异常的 value 信息为: returned NULL without setting an error,提示你返回了 NULL 但没有设置 error。因为返回 NULL 表示程序需要终止了,那么就应该把为什么需要终止的理由告诉使用者。
然后我们来测试一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import kagura_nanatry : kagura_nana.f1() except Exception as e: print (e) try : kagura_nana.f1(1 , 2 , 3 , 4 ) except Exception as e: print (e) try : kagura_nana.f1(1 , 2 , 3 ) except Exception as e: print (e) lst = ["xx" , "yy" ] print (kagura_nana.f1(123 , "123" , lst)) print (lst)
所表现的一切,都和我们在底层设置的一样。另外我们再来看看这个函数的身份是什么:
1 2 3 4 5 6 7 import kagura_nanadef foo (): pass print (kagura_nana.f1) print (sum ) print (foo)
我们居然实现了一个内置函数,怎么样是不是很神奇呢?因为扩展模块里面的函数和解释器内置的函数本质上都是一样的,所以它们都是 built-in。
返回布尔类型和 None 我们说函数都必须返回一个 PyObject *,如果这个函数没有返回值,那么在 Python 中实际上返回的是一个 None,但是我们不能返回 NULL,None 和 NULL 是两码事。在扩展函数中,如果返回 NULL 就表示这个函数执行的时候,不符合某个逻辑,我们需要终止掉,不能再执行下去了。这是在底层,但是在 Python 的层面,你需要告诉使用者为什么不能执行了,或者说底层的哪一行代码不满足条件,因此这个时候我们会在 return NULL 之前需要手动设置一个异常,这样在 Python 代码中才知道为什么底层函数退出了。当然有时候会自动帮我们设置,比如们说的 PyArg_ParseTuple。
那么在底层如何返回一个 None 呢?既然要返回我们就需要知道它的结构是什么。
这个 NoneType 在底层对应的是 _PyNone_Type,至于 None 在底层对应的结构体是 _Py_NoneStruct,所以我们返回的时候应该返回这个结构体的指针。不过官方不推荐直接使用,而是给我们定义了一个宏,#define Py_None (&_Py_NoneStruct)
,我们直接返回 Py_None 即可。
不光是 None,我们说还有 True 和 False,True 和 False 对应的结构体是:_Py_FalseStruct,_Py_TrueStruct,它们本质上是 PyLongObject,Python 也不推荐直接返回,也是定义了两个宏。
#define Py_False ((PyObject *) &_Py_FalseStruct)
#define Py_True ((PyObject *) &_Py_TrueStruct)
推荐我们使用 Py_False 和 Py_True。
另外:
return Py_None; 等价于 Py_RETURN_NONE;
return Py_True; 等价于 Py_RETURN_TRUE;
return Py_False; 等价于 Py_RETURN_FALSE;
可以自己测试一下,比如条件满足返回 Py_True,不满足返回 Py_False 等等。
传递关键字参数 我们上面的例子都是通过位置参数实现的,如果我们通过关键字参数传递呢?很明显是会报错的,因为我们参数名叫什么都不知道,所以上面的例子都不支持关键字参数。那么下面我们就来看看关键字参数要如何实现。
传递关键字参数的话,我们是通过 key=value 的方式来实现,那么在 C 中我们如何解析呢?既然支持关键字的方式,那么是不是也可以实现默认参数呢?答案是肯定的,我们知道解析位置参数是通过 PyArg_ParseTuple,而解析关键字参数是通过 PyArg_ParseTupleAndKeywords。
1 函数原型: int PyArg_ParseTupleAndKeywords(PyObject *args, PyObject *kw, const char *format, char *keywords[], ...)
我们看到相比原来的 PyArg_ParseTuple,多了一个 kw 和一个 char * 类型的数组,具体怎么用我们在编写代码的时候说。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 #include "Python.h" static PyObject *f1 (PyObject *self, PyObject *args, PyObject *kwargs) { wchar_t *name; int age = 17 ; wchar_t *gender = L"FEMALE" ; char *keys[] = {"name" , "age" , "gender" , NULL }; if (!PyArg_ParseTupleAndKeywords(args, kwargs, "u|iu" , keys, &name, &age, &gender)){ return NULL ; } wchar_t res[100 ]; swprintf(res, 100 , L"name: %s, age: %d, gender: %s" , name, age, gender); return PyUnicode_FromWideChar(res, wcslen(res)); } static PyMethodDef methods[] = { { "f1" , (PyCFunction) f1, METH_VARARGS | METH_KEYWORDS, NULL }, {NULL , NULL , 0 , NULL } }; static PyModuleDef module = { PyModuleDef_HEAD_INIT, "kagura_nana" , "this is a module named kagura_nana" , -1 , methods, NULL , NULL , NULL , NULL }; PyMODINIT_FUNC PyInit_kagura_nana (void ) { return PyModule_Create(&module); }
用 Python 来测试一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import kagura_nanatry : print (kagura_nana.f1()) except Exception as e: print (e) try : print (kagura_nana.f1(123 )) except Exception as e: print (e) print (kagura_nana.f1("古明地觉" )) print (kagura_nana.f1("古明地恋" , 16 )) print (kagura_nana.f1("古明地恋" , 16 , "女" ))
我们看到一切都符合我们的预期,而且 PyArg_ParseTuple,和 PyArg_ParseTupleAndKeywords 可以自动帮我们检测参数是否合法,不合法抛出合理的异常。当然你也可以检测参数的个数,或者将参数一个一个获取、用 PyXxx_Check 系列检测函数进行判断,看看是否符合预期,当然这么做就比较麻烦了。
PyArg_ParseTuple 和 PyArg_ParseTupleAndKeywords 里面的占位符还可以接收一些特殊的符号,我们举个栗子。为了更好的说明,我们统一以 PyArg_ParseTupleAndKeywords 为例。
占位符 : 下面的是之前写的 C 代码,我们不做任何改动,来测试一下当参数传递错误时的报错信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include "Python.h" static PyObject *f1 (PyObject *self, PyObject *args, PyObject *kwargs) { wchar_t *name; int age = 17 ; wchar_t *gender = L"FEMALE" ; char *keys[] = {"name" , "age" , "gender" , NULL }; if (!PyArg_ParseTupleAndKeywords(args, kwargs, "u|iu" , keys, &name, &age, &gender)){ return NULL ; } wchar_t res[100 ]; swprintf(res, 100 , L"name: %s, age: %d, gender: %s" , name, age, gender); return PyUnicode_FromWideChar(res,wcslen(res)); } static PyMethodDef methods[] = { { "f1" , (PyCFunction) f1, METH_VARARGS | METH_KEYWORDS, NULL }, {NULL , NULL , 0 , NULL } }; static PyModuleDef module = { PyModuleDef_HEAD_INIT, "kagura_nana" , "this is a module named kagura_nana" , -1 , methods, NULL , NULL , NULL , NULL }; PyMODINIT_FUNC PyInit_kagura_nana (void ) { return PyModule_Create(&module); }
我们用 Python 来测试一下,注意观察报错信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import kagura_nanatry : print (kagura_nana.f1()) except Exception as e: print (e) try : print (kagura_nana.f1("古明地觉" , xxx=123 )) except Exception as e: print (e) try : print (kagura_nana.f1("古明地觉" , name=123 )) except Exception as e: print (e)
报错信息似乎没有什么特别的,但是注意了,我们来做一下改动。
1 2 3 if (!PyArg_ParseTupleAndKeywords(args, kwargs, "u|iu:abcdefg" , keys, &name, &age, &gender)){ return NULL ; }
其它地方都不变,我们只在 format 字符串的结尾加上了一个 :abcdefg
,然后编译再来测试一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import kagura_nanatry : print (kagura_nana.f1()) except Exception as e: print (e) try : print (kagura_nana.f1("古明地觉" , xxx=123 )) except Exception as e: print (e) try : print (kagura_nana.f1("古明地觉" , name=123 )) except Exception as e: print (e)
你看到了什么?没错,默认的报错信息使用的是 function,但我们通过在占位符中指定 :xxx
,可以将 function 变成我们指定的内容 xxx,一般和函数名保持一致。另外需要注意的是,:xxx
要出现在占位符的结尾,并且只能出现一次。如果这样的话会变成什么样子呢?
1 PyArg_ParseTupleAndKeywords(args, kwargs, "u:aaa|iu:abcdefg", keys, &name, &age, &gender)
显然这变成了只接受一个参数,然后我们将参数不对时、返回报错信息中的 function 换成了 aaa|iu:abcdefg
。并且你在传递参数的时候还会报出如下错误:
1 SystemError: More keyword list entries (3) than format specifiers (1)
因为占位符中相当于只有一个 u,也就是接收一个参数,但是我们后面跟了 &name、&age、&gender。关键字 entry 是 3,占位符是 1,两者不匹配。因此 :xxx
一定要出现在最后面,并且只能出现一次。
另外,即使函数不接收参数我们也是可以这么做的,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #include "Python.h" static PyObject *f1 (PyObject *self, PyObject *args, PyObject *kwargs) { char *keys[] = {NULL }; if (!PyArg_ParseTupleAndKeywords(args, kwargs, "" , keys)){ return NULL ; } Py_INCREF(Py_None); return Py_None; } static PyMethodDef methods[] = { { "f1" , (PyCFunction) f1, METH_VARARGS | METH_KEYWORDS, NULL }, {NULL , NULL , 0 , NULL } }; static PyModuleDef module = { PyModuleDef_HEAD_INIT, "kagura_nana" , "this is a module named kagura_nana" , -1 , methods, NULL , NULL , NULL , NULL }; PyMODINIT_FUNC PyInit_kagura_nana (void ) { return PyModule_Create(&module); } import kagura_nana try: print(kagura_nana.f1("xxx" )) except Exception as e: print(e) # function takes at most 0 arguments (1 given)
然后我们加上 :xxx
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 static PyObject *f1 (PyObject *self, PyObject *args, PyObject *kwargs) { char *keys[] = {NULL }; if (!PyArg_ParseTupleAndKeywords(args, kwargs, ":123" , keys)){ return NULL ; } Py_INCREF(Py_None); return Py_None; } import kagura_nana try: print(kagura_nana.f1("xxx" )) except Exception as e: print(e) # 123 () takes at most 0 arguments (1 given)
我们看到返回信息也被我们修改了,以上就是 :xxx
的作用。所以目前我们看到了两个特殊符号,一个是 |
用来实现默认参数,一个是这里的 :
用来自定义报错信息中的函数名。
占位符 ! 我们说占位符 O 表示接收一个 Python 中的对象,但这个对象显然是没有限制的,可以是列表、可以是字典等等。我们之前是通过 Check 的方式进行检测,但是 Python 底层为我们提供更简便的做法,先来看一个常规的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 static PyObject *f1 (PyObject *self, PyObject *args, PyObject *kwargs) { char *keys[] = {"val1" , "val2" , "val3" , NULL }; PyObject *val1; PyObject *val2; PyObject *val3; if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OOO" , keys, &val1, &val2, &val3)){ return NULL ; } Py_INCREF(Py_None); return Py_None; }
这个例子很简单,就是接收三个 PyObject *,但如果我希望第一个参数的类型是浮点型,第三个参数的类型是字典,这个时候该怎么做呢?此时 ! 就派上用场了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 static PyObject *f1 (PyObject *self, PyObject *args, PyObject *kwargs) { char *keys[] = {"val1" , "val2" , "val3" , NULL }; PyObject *val1; PyObject *val2; PyObject *val3; if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!OO!:my_func" , keys, &PyFloat_Type, &val1, &val2, &PyDict_Type, &val3)){ return NULL ; } Py_INCREF(Py_None); return Py_None; }
然后其它地方不变,我们来编译测试一下。
1 2 3 4 5 6 7 8 9 10 11 import kagura_nanatry : print (kagura_nana.f1(123 , 123 , "xx" )) except Exception as e: print (e) try : print (kagura_nana.f1(123.0 , 11 , "xx" )) except Exception as e: print (e)
这个功能就很方便了,可以让我们更加轻松地限制参数类型。但如果你用过 Cython 的话,你会发现我这里所说的方便实在是不敢恭维。如果你要写扩展,那么我强烈推荐 Cython,而且用 Cython 可以轻松的连接 C / C++。
注意:! 只能跟在 O 的后面。
占位符 & & 的话,对于我们编写扩展而言用的不是很多,首先 & 和 上面说的 ! 用法类似,并且都只能跟在 O 的后面。O! 的话,我们说会对应一个类型指针和一个 PyObject *(参数就会传递给它),会判断传递的参数的类型是否和指定的类型一致。但 O& 的话,则是对应一个函数(convert)和一个任意类型的指针(address),会执行 convert(object, address)
,这个 object 就是我们传递过来的参数。我们举个栗子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void convert (PyObject *object, long *any) { *any = PyLong_AsLong(object); } static PyObject *f1 (PyObject *self, PyObject *args, PyObject *kwargs) { char *keys[] = {"val1" , NULL }; long any = 0 ; if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&" , keys, convert, &any)){ return NULL ; } printf ("any = %ld\n" , any + 1 ); Py_INCREF(Py_None); return Py_None; }
我们来测试一下:
1 2 3 4 5 print (kagura_nana.f1(123 ))""" any = 124 None """
效果大概就是这样,个人觉得对于我们编写扩展而言用处不是很大,了解一下即可。
占位符 ; 占位符 ;
和 :
比较类似,但 ;
更加粗暴。至于怎么个粗暴法,看个栗子就一目了然了。
1 2 3 4 5 6 7 8 9 10 11 12 static PyObject *f1 (PyObject *self, PyObject *args, PyObject *kwargs) { char *keys[] = {"val1" , NULL }; PyObject *val1; if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!;my name is van, i am a artist, a performance artist" , keys, &PyFloat_Type, &val1)){ return NULL ; } Py_INCREF(Py_None); return Py_None; }
然后我们来调用试试,看看会有什么结果:
1 2 3 4 5 6 7 8 9 10 11 import kagura_nanatry : print (kagura_nana.f1()) except Exception as e: print (e) try : print (kagura_nana.f1(123 , 123 )) except Exception as e: print (e)
目前来看的话,似乎一切正常,但是往下看:
此时把整个报错信息都给修改了,因此这个符号也不是很常用。
注意:;
同样需要放到结尾,并且和 :
相互排斥,两者不可同时出现。
占位符 $ 老规矩,还是先来看一个常规的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static PyObject *f1 (PyObject *self, PyObject *args, PyObject *kwargs) { char *keys[] = {"val1" , "val2" , "val3" , NULL }; PyObject *val1; PyObject *val2; PyObject *val3; if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OOO" , keys, &val1, &val2, &val3)){ return NULL ; } Py_INCREF(Py_None); return Py_None; } import kagura_nana print (kagura_nana.f1(123 , 123 , 123 )) print (kagura_nana.f1(123 , val2=123 , val3=123 )) print (kagura_nana.f1(123 , 123 , val3=123 )) print (kagura_nana.f1(val1=123 , val2=123 , val3=123 ))
以上都是没有问题的,可以通过位置参数传递、也可以通过关键字参数传递,只要位置参数在关键字参数之前即可。但如果我们希望某个参数只能通过关键字的方式传递呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 static PyObject *f1 (PyObject *self, PyObject *args, PyObject *kwargs) { char *keys[] = {"val1" , "val2" , "val3" , NULL }; PyObject *val1; PyObject *val2; PyObject *val3; if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OO$O" , keys, &val1, &val2, &val3)){ return NULL ; } Py_INCREF(Py_None); return Py_None; }
重新编译然后测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import kagura_nanaprint (kagura_nana.f1(123 , val2=123 , val3=123 ))print (kagura_nana.f1(123 , 123 , val3=123 ))print (kagura_nana.f1(val1=123 , val2=123 , val3=123 ))try : kagura_nana.f1(123 , 123 , 123 ) except Exception as e: print (e) def f1 (val1, val2, *, val3 ): return None
不过有一点需要注意,目前来说,如果 |
和 $
同时出现的话,那么 |
必须要在 $
的前面。所以如果既有仅限关键字参数、又有可选参数,那么仅限关键字参数必须同时也是可选参数,所以 |
要在 $
的前面。如果我们把 |
写在了 $
的后面,那么执行会抛异常。
并且,即便仅限关键字参数和默认参数相同,那也应该这么写 OO|$O
,而不能这么写 OO$|O
。
占位符 这个 # 不可以跟在 O 后面,它是跟在 s 或者 u 后面,用来限制长度,有兴趣自己去了解一下。
Py_BuildValue 下面介绍一个非常方便的函数 Py_BuildValue,专门用来对数据进行打包的,返回一个 PyObject *,同样是通过占位符的方式。
Py_BuildValue 的占位符和 PyArg_ParseTuple 里面的占位符是一致的,只不过功能相反。比如:i,PyArg_ParseTuple 是将 Python 中的 int 转成 C 中的 int,而 Py_BuildValue 是将 C 中的 int 打包成 Python 中的 int。所以它们的占位符一致,功能正好相反,并且我们在介绍 PyArg_ParseTuple 的时候只介绍一部分占位符,其实支持的占位符不止我们上面说的那些,下面就来罗列一下。
再重复一次,PyArg_ParseTuple 和 Py_BuildValue 的占位符是一致的,但是功能相反。
我们只接用官方的栗子,因为官方给的栗子非常直观。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Py_BuildValue("" ) None Py_BuildValue ("i" , 123 ) 123Py_BuildValue ("iii" , 123 , 456 , 789 ) (123 , 456 , 789 ) Py_BuildValue ("s" , "hello" ) 'hello'Py_BuildValue ("y" , "hello" ) b'hello'Py_BuildValue ("ss" , "hello" , "world" ) ('hello' , 'world' ) Py_BuildValue ("s#" , "hello" , 4 ) 'hell'Py_BuildValue ("y#" , "hello" , 4 ) b'hell'Py_BuildValue ("()" ) () Py_BuildValue ("(i)" , 123 ) (123 ,) Py_BuildValue ("(ii)" , 123 , 456 ) (123 , 456 ) Py_BuildValue ("(i,i)" , 123 , 456 ) (123 , 456 ) Py_BuildValue ("[i,i]" , 123 , 456 ) [123, 456]Py_BuildValue ("{s:i,s:i}" , "abc" , 123 , "def" , 456 ) {'abc' : 123 , 'def' : 456 }Py_BuildValue("((ii)(ii)) (ii)" , 1 , 2 , 3 , 4 , 5 , 6 ) (((1 , 2 ), (3 , 4 )), (5 , 6 ))
如果是多个符号,自动会变成一个元组。我们来测试一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 #include "Python.h" static PyObject *f1 (PyObject *self, PyObject *args, PyObject *kwargs) { PyObject *lst = PyList_New(5 ); PyList_SetItem(lst, 0 , Py_BuildValue("i" , 123 )); PyList_SetItem(lst, 1 , Py_BuildValue("is" , 123 , "hello matsuri" )); PyList_SetItem(lst, 2 , Py_BuildValue("[i, i]" , 123 , 321 )); PyList_SetItem(lst, 3 , Py_BuildValue("(s)s" , "hello" , "matsuri" )); PyList_SetItem(lst, 4 , Py_BuildValue("{s: s}" , "hello" , "matsuri" )); return lst; } static PyMethodDef methods[] = { { "f1" , (PyCFunction) f1, METH_VARARGS | METH_KEYWORDS, NULL }, {NULL , NULL , 0 , NULL } }; static PyModuleDef module = { PyModuleDef_HEAD_INIT, "kagura_nana" , "this is a module named kagura_nana" , -1 , methods, NULL , NULL , NULL , NULL }; PyMODINIT_FUNC PyInit_kagura_nana (void ) { return PyModule_Create(&module); } from pprint import pprint import kagura_nana pprint (kagura_nana.f1()) """ [123, (123 , 'hello matsuri' ) , [123, 321], (('hello' ,), 'matsuri' ) , {'hello' : 'matsuri' }] "" "
我们看到结果是符合我们的预期的,另外除了 Py_BuildValue 之外,还有一个 PyTuple_Pack,这两者是类似的,只不过后者只接收 PyObject *,举个栗子就很清晰了:
1 Py_BuildValue("OO", a, b) 等价于 PyTuple_Pack(2, a, b)
这个是固定打包成元组,而且第一个参数是个数,不是 format,因此它不支持通过占位符来指定元素类型,而是只接收 PyObject *。
操作 PyDictObject Python 中的字典在底层要如何读取、如何设置,这个我们必须要好好地说一说。像整型、浮点型、字符串、元组、列表、集合,它们都比较简单,我们就不详细说了。比如列表:Python 中插入元素是调用 insert,那么底层则是 PyList_Insert;追加元素是 append,那么底层则是 PyList_Append;设置元素是 __setitem__,那么底层则是 PyList_SetItem;同理获取元素是 PyList_GetItem,写法非常具有规范性。所以如果不知道某个 API 的话,可以去查看解释的源码,比如你想查看元组,那么就去 Include/tupleobject.h 中查看:
像这些凡是以 PyAPI 开头的都是可以直接用的,PyAPI_DATA 表示数据,PyAPI_FUNC 表示函数,至于它们的含义是什么,我们可以通过文档查看。在 Python 的安装目录的 Doc 目录下就有,点击通过关键字进行检索即可。当然基本数据类型的一些方法,相信通过函数名即可判断,比如:PyTuple_GetItem,很明显就是通过索引获取元素的。还是那句话,Python 解释器的整个工程,在命名方面都非常有规律。
所以我们的重点是字典的使用,因为字典比较特殊,它里面的键值对的形式,而列表、元组等容器里面的元素是单一独立的。
PyDictObject 的读取 先来介绍内部关于读取的一些 API:
PyDict_Contains(dic, key):判断字典中是否具有某个 key
PyDict_GetItem(dic, key):获取字典中某个 key 对应的 value
PyDict_GetItemString(dic, key):和 PyDict_GetItem 作用相同,但这里的 key 是一个 char *
PyDict_Keys(dic):获取所有的 key
PyDict_Values(dic):获取所有的 value
PyDict_Items(dic):获取所有的 key-value
下面我们来操作一波:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 #include "Python.h" static PyObject *f1 (PyObject *self, PyObject *args, PyObject *kwargs) { PyObject *dic; char *keys[] = {"dic" , NULL }; if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!" , keys, &PyDict_Type, &dic)){ return NULL ; } PyObject *res; PyObject *name = PyUnicode_FromString("name" ); if (!PyDict_Contains(dic, name)){ res = PyUnicode_FromString("key `name` does not exists" ); } else { res = PyDict_GetItem(dic, name); Py_INCREF(res); } Py_DECREF(name); return res; } static PyMethodDef methods[] = { { "f1" , (PyCFunction) f1, METH_VARARGS | METH_KEYWORDS, NULL }, {NULL , NULL , 0 , NULL } }; static PyModuleDef module = { PyModuleDef_HEAD_INIT, "kagura_nana" , "this is a module named kagura_nana" , -1 , methods, NULL , NULL , NULL , NULL }; PyMODINIT_FUNC PyInit_kagura_nana (void ) { return PyModule_Create(&module); } import kagura_nana try: print(kagura_nana.f1("" )) except Exception as e: print(e) # argument 1 must be dict, not str print(kagura_nana.f1({})) # key `name` does not exists print(kagura_nana.f1({"name" : "古明地觉" })) # 古明地觉
PyDictObject 的遍历 首先我们说可以通过 PyDict_Keys、PyDict_Values、PyDict_Items 来进行遍历,下面演示一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 static PyObject *f1 (PyObject *self, PyObject *args, PyObject *kwargs) { PyObject *dic; char *keys[] = {"dic" , NULL }; if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!" , keys, &PyDict_Type, &dic)){ return NULL ; } PyObject *res = PyList_New(3 ); PyList_SetItem(res, 0 , PyDict_Keys(dic)); PyList_SetItem(res, 1 , PyDict_Values(dic)); PyList_SetItem(res, 2 , PyDict_Items(dic)); return res; } import kagura_nana print (kagura_nana.f1({"name" : "satori" , "age" : 17 })) """ [['name', 'age'], ['satori', 17], [('name' , 'satori' ) , ('age' , 17 ) ]] """
而且我们看到 PyDict_Keys 等函数返回的是列表,这说明创建了一个新的空间,引用计数为 1。但我们没有调用 Py_DECREF,这是因为我们将其放在了一个新的列表中,如果作为某个容器的元素,那么引用计数也应该要增加。但对于 PyListObject、PyTupleObject 而言,通过 PyList_SetItem、PyTuple_SetItem 是不会增加指向对象的引用计数的,所以结果正好抵消,我们不需要对引用计数做任何处理。
但如果我们是通过 PyList_Append 进行追加、或者 PyList_Insert 进行插入的话,那么是会增加引用计数的,这样引用计数就增加了 2,因此我们还需要减去 1。所以这一点比较烦人,因为你光知道何时增加引用计数、何时减少引用计数还是不够的,你还要看某一个操作到底有没有增加、或者减少。就拿我们这里设置元素为例,本来作为容器内的一个元素,理论上是要增加引用计数的,但是结果却没有增加。而添加和插入元素,也是作为容器的一个元素,但是这两个操作却增加了。所以还是推荐 Cython,再度安利一波,写扩展用 Cython 真的非常香。
这里我们将元素都获取出来了,至于遍历也很简单,这里不测试了。
PyDictObject 的设置和删除
PyDict_SetItem(dic, key, value):设置元素
PyDict_DelItem(dic, key, value):删除元素
PyDict_Clear(dic):清空字典
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 static PyObject *f1 (PyObject *self, PyObject *args, PyObject *kwargs) { PyObject *dic; char *keys[] = {"dic" , NULL }; if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!" , keys, &PyDict_Type, &dic)){ return NULL ; } PyObject *key = PyUnicode_FromString("name" ); PyObject *value = PyUnicode_FromString("satori" ); PyDict_SetItem(dic, key, value); Py_XDECREF(key); Py_XDECREF(value); key = PyUnicode_FromString("age" ); if (PyDict_Contains(dic, key)) { PyDict_DelItem(dic, key); } Py_XDECREF(key); Py_INCREF(Py_None); return Py_None; }
测试一下:
1 2 3 4 5 import kagura_nanadic = {"name" : "mashiro" , "age" : 17 } kagura_nana.f1(dic) print (dic)
当然还有很多其它 API,可以查看源代码(Include/dictobject.h)自己测试一下。
编写扩展类 我们之前在 C 中编写的都是函数,但光有函数显然是不够的,我们需要实现类。而在 C 中实现的类被称为扩展类,它和 Python 内置的类(int、dict、str等等)是等价的,都属于静态类,直接指向了 C 一级的数据结构。
下面来看看在 C 中如何实现扩展类,首先我们来实现一个最基本的扩展类,也就是只包含一些最关键的部分。然后再添加类参数、方法,以及继承等等。
当然最重要的一点,我们还要解决类的循环引用、以及自定义垃圾回收。像列表、元组、字典等容器,它们也都会发生循环引用。
前面有一点我们没有提,当一个容器(比如列表)引用计数减一的时候,里面的元素(指向的对象)的引用计数是不会发生改变的。只有当一个容器的引用计数为 0 被销毁的时候,在销毁之前会先将内部元素的引用计数都减 1,然后再销毁这个容器。
而循环引用是引用计数机制所面临的最大的痛点,所以 Python 中的 gc 就是来干这个事情的,通过分代技术根据对象的生命周期划分为三个链表,然后通过三色标记模型来找出那些具有循环引用的对象,改变它们的引用计数。所以在 Python 中一个对象是否要被回收,最终还是取决于它的引用计数是否为 0。如果是 Python 代码的话,我们在实现类的时候,解释器会自动帮我们处理这一点,但我们是做类扩展,因此这些东西就必须由我们来考虑了。
编写扩展类前奏曲 我们之前编写了扩展函数,我们说首先要创建一个模块,这里也是一样的,因为类也要在模块里面。编写函数是有套路的,编写类也是一样,我们还是先看看大致的流程,具体细节会在慢慢补充。
首先我们需要了解以下内容:
1. 一个类要有类名、构造函数、析构函数
2. 所有的类在底层都是一个 PyTypeObject 实例,而且类也是一个对象
3. PyType_Ready 对类进行初始化,主要是进行属性字典的设置
4. PyModule_AddObject,将扩展类添加到模块中
那么一个类在底层都有哪些属性呢?很明显,我们说所有的类都是一个 PyTypeObject 实例,那么我们就把这个结构体拷贝出来看一下就知道了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 typedef struct _typeobject { PyObject_VAR_HEAD const char *tp_name; Py_ssize_t tp_basicsize, tp_itemsize; destructor tp_dealloc; printfunc tp_print; getattrfunc tp_getattr; setattrfunc tp_setattr; PyAsyncMethods *tp_as_async; reprfunc tp_repr; PyNumberMethods *tp_as_number; PySequenceMethods *tp_as_sequence; PyMappingMethods *tp_as_mapping; hashfunc tp_hash; ternaryfunc tp_call; reprfunc tp_str; getattrofunc tp_getattro; setattrofunc tp_setattro; PyBufferProcs *tp_as_buffer; unsigned long tp_flags; const char *tp_doc; traverseproc tp_traverse; inquiry tp_clear; richcmpfunc tp_richcompare; Py_ssize_t tp_weaklistoffset; getiterfunc tp_iter; iternextfunc tp_iternext; struct PyMethodDef *tp_methods ; struct PyMemberDef *tp_members ; struct PyGetSetDef *tp_getset ; struct _typeobject *tp_base ; PyObject *tp_dict; descrgetfunc tp_descr_get; descrsetfunc tp_descr_set; Py_ssize_t tp_dictoffset; initproc tp_init; allocfunc tp_alloc; newfunc tp_new; freefunc tp_free; inquiry tp_is_gc; PyObject *tp_bases; PyObject *tp_mro; PyObject *tp_cache; PyObject *tp_subclasses; PyObject *tp_weaklist; destructor tp_del; unsigned int tp_version_tag; destructor tp_finalize; } PyTypeObject;
这里面我们看到有很多成员,如果有些成员我们不需要的话,那么就设置为 0 即可。不过即便设置为 0,但是有些成员我们在调用 PyType_Ready 初始化的时候,也会设置进去。比如 tp_dict,这个我们创建类的时候没有设置,但是这个类是有属性字典的,因为在 PyType_Ready 中设置了;但有的不会,比如 tp_dictoffset,这个我们没有设置,那么类在 PyType_Ready 中也不会设置,因此这个类的实例对象,就真的没有属性字典了。再比如 tp_free,我们也没有设置,但是是可以调用的,原因你懂的。
虽然里面的成员非常多,但是我们在实现的时候不一定每一个成员都要设置。如果只需要指定某几个成员的话,那么我们可以先创建一个 PyTypeObject 实例,然后针对指定的属性进行设置即可。
下面我们来编写一个简单的扩展类,具体细节在代码中体现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 #include "Python.h" class MyClass {public: PyObject_HEAD }; static PyObject *MyClass_new (PyTypeObject *cls, PyObject *args, PyObject *kw) { MyClass *self = (MyClass *)cls -> tp_alloc(cls, 0 ); return (PyObject *)self; } static int MyClass_init (PyObject *self, PyObject *args, PyObject *kw) { char *name; int age; char *gender; char *keys[] = {"name" , "age" , "gender" , NULL }; if (!PyArg_ParseTupleAndKeywords(args, kw, "sis" , keys, &name, &age, &gender)){ return -1 ; } printf ("name = %s, age = %d, gender = %s\n" , name, age, gender); return 0 ; } void MyClass_del (PyObject *self) { printf ("call __del__\n" ); Py_TYPE(self) -> tp_free(self); } static PyModuleDef module = { PyModuleDef_HEAD_INIT, "kagura_nana" , "this is a module named hanser" , -1 , 0 , NULL , NULL , NULL , NULL }; PyMODINIT_FUNC PyInit_kagura_nana (void ) { static PyTypeObject cls; PyVarObject ob_base = {1 , &PyType_Type, 0 }; cls.ob_base = ob_base; cls.tp_name = "MyClass" ; cls.tp_basicsize = sizeof (MyClass); cls.tp_itemsize = 0 ; cls.tp_new = MyClass_new; cls.tp_init = MyClass_init; cls.tp_dealloc = MyClass_del; if (PyType_Ready(&cls) < 0 ){ return NULL ; } Py_XINCREF(&cls); PyObject *m = PyModule_Create(&module); PyModule_AddObject(m, "MyClass" , (PyObject *)&cls); return m; }
然后是用于编译的 py 文件:
1 2 3 4 5 6 7 8 9 10 from distutils.core import *setup( name="kagura_nana" , version="1.11" , author="古明地盆" , author_email="66666@东方地灵殿.com" , ext_modules=[Extension("kagura_nana" , ["main.cpp" ])], )
注意:之前使用的都是自己住的地方的台式机,里面装了相应的环境,因为机器性能比较好。但是春节本人回家了,现在使用的是自己的笔记本,而笔记本里面没有装 Visual Studio 等环境,因此接下来环境会选择我阿里云上的 CentOS。
编译的方式跟之前一样,只不过需要先执行一下 yum install gcc-c++
,否则编译时会抛出:
1 gcc: error trying to exec 'cc1plus': execvp: No such file or directory
如果你已经装了,那么是没有问题的,但也建议执行确认一下。下面操作一波:
1 2 3 4 5 6 7 8 9 10 11 12 >>> import kagura_nana>>> kagura_nana<module 'kagura_nana' from '/usr/local/lib64/python3.6/site-packages/kagura_nana.cpython-36m-x86_64-linux-gnu.so' > >>> try :... ... ... self = kagura_nana.MyClass()... except Exception as e:... print (e)... call __del__ Required argument 'name' (pos 1 ) not found
尽管实例化失败,但是这个对象在 new 方法中被创建了,所以依旧会调用 __del__。然后我们传递参数,但是我们在构造函数中只是打印,并没有设置到 self 中。
1 2 3 4 5 6 >>> self = kagura_nana.MyClass("mashiro" , 16 , "female" )name = mashiro, age = 16 , gender = female >>> self.nameTraceback (most recent call last): File "<stdin>" , line 1 , in <module> AttributeError: 'MyClass' object has no attribute 'name'
我们看到调用失败了,因为我们没有设置到 self 中,然后再看看析构函数。
1 2 3 >>> del selfcall __del__ >>>
成功调用,然后里面的 printf 也成功执行。
给实例对象添加属性 整体流程我们大致了解了,下面看看如何给实例对象添加属性。我们说 PyTypeObject 里面有一个 tp_members 属性,很明显它就是用来指定实例对象的属性的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 #include "Python.h" #include "structmember.h" class MyClass {public: PyObject_HEAD PyObject *name; PyObject *age; PyObject *gender; }; static PyObject *MyClass_new (PyTypeObject *cls, PyObject *args, PyObject *kw) { MyClass *self = (MyClass *)cls -> tp_alloc(cls, 0 ); return (PyObject *)self; } static int MyClass_init (PyObject *self, PyObject *args, PyObject *kw) { PyObject *name; PyObject *age = NULL ; PyObject *gender = NULL ; char *keys[] = {"name" , "age" , "gender" , NULL }; if (!PyArg_ParseTupleAndKeywords(args, kw, "O!|O!O!" , keys, &PyUnicode_Type, &name, &PyLong_Type, &age, &PyUnicode_Type, &gender)){ return -1 ; } Py_XINCREF(name); if (age) Py_XINCREF(age); else age = PyLong_FromLong(17 ); if (gender) Py_XINCREF(gender); else gender = PyUnicode_FromWideChar(L"萌妹子" , 3 ); ((MyClass *)self) -> name = name; ((MyClass *)self) -> age = age; ((MyClass *)self) -> gender = gender; return 0 ; } void MyClass_del (PyObject *self) { Py_XDECREF(((MyClass *)self) -> name); Py_XDECREF(((MyClass *)self) -> age); Py_XDECREF(((MyClass *)self) -> gender); Py_TYPE(self) -> tp_free(self); } static PyModuleDef module = { PyModuleDef_HEAD_INIT, "kagura_nana" , "this is a module named hanser" , -1 , 0 , NULL , NULL , NULL , NULL }; PyMODINIT_FUNC PyInit_kagura_nana (void ) { static PyTypeObject cls; PyVarObject ob_base = {1 , &PyType_Type, 0 }; cls.ob_base = ob_base; cls.tp_name = "MyClass" ; cls.tp_basicsize = sizeof (MyClass); cls.tp_itemsize = 0 ; cls.tp_new = MyClass_new; cls.tp_init = MyClass_init; cls.tp_dealloc = MyClass_del; static PyMemberDef members[] = { { "name" , T_OBJECT_EX, offsetof(MyClass, name), 0 , "this is a name" }, {"age" , T_OBJECT_EX, offsetof(MyClass, age), 1 , "this is a age" }, {"gender" , T_OBJECT_EX, offsetof(MyClass, gender), 0 , "this is a gender" }, {NULL } }; cls.tp_members = members; if (PyType_Ready(&cls) < 0 ){ return NULL ; } Py_XINCREF(&cls); PyObject *m = PyModule_Create(&module); PyModule_AddObject(m, "MyClass" , (PyObject *)&cls); return m; }
我们来测试一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 >>> import kagura_nana>>> self = kagura_nana.MyClass("古明地觉" )>>> self.name, self.age, self.gender('古明地觉' , 17 , '萌妹子' ) >>> >>> self = kagura_nana.MyClass("古明地恋" , 16 , "美少女" )>>> self.name, self.age, self.gender('古明地恋' , 16 , '美少女' ) >>> >>> self.name, self.gender = "koishi" , "びしょうじょ" >>> self.name, self.age, self.gender('koishi' , 16 , 'びしょうじょ' ) >>> >>> ... >>> self.age = 16 Traceback (most recent call last): File "<stdin>" , line 1 , in <module> AttributeError: readonly attribute >>>
一切正常,并且我们看到 age 是只读的,因为我们在 PyMemberDef 中将其设置为只读,我们来看一下这个结构体。该结构体的定义藏身于 *Include/structmember.h* 中。
1 2 3 4 5 6 7 typedef struct PyMemberDef { const char *name; int type; Py_ssize_t offset; int flags; const char *doc; } PyMemberDef;
然后我们重点看一下里面的 type 成员,它表示属性的类型,支持如下选项:
#define T_SHORT 0
#define T_INT 1
#define T_LONG 2
#define T_FLOAT 3
#define T_DOUBLE 4
#define T_STRING 5
#define T_OBJECT 6
#define T_CHAR 7
#define T_BYTE 8
#define T_UBYTE 9
#define T_USHORT 10
#define T_UINT 11
#define T_ULONG 12
#define T_STRING_INPLACE 13
#define T_BOOL 14
#define T_OBJECT_EX 16
#define T_LONGLONG 17
#define T_ULONGLONG 18
#define T_PYSSIZET 19
#define T_NONE 20
我们的类(MyClass)中的成员应该是 PyObject *,但是用来接收参数的变量可以不是,只不过在设置实例属性的时候需要再转成 PyObject *,如果接收的就是 PyObject *,那么就不需要再转了。而上面这些描述的就是参数的类型,所以我们一般用 T_OBJECT_EX 即可,但是还有一个 T_OBJECT,这两者的区别是前者如果接收的是 NULL(没有接收到值),那么会引发一个 AttributeError。
到目前为止,我们应该感受到使用 C/C++ 来写扩展是一件多么痛苦的事情,特别是引用计数,一搞不好就出现内存泄漏或者悬空指针。因此,关键来了,再次安利一波 Cython。
除了 init__、__new__、__del 之外,你还可以添加其它的方法,比如 tp_call、tp_getset 等等。
给类添加成员 一个类里面可以定义很多的函数,那么这在 C 中是如何实现的呢?很简单,和模块中定义函数是一致的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 #include "Python.h" #include "structmember.h" class MyClass { public: PyObject_HEAD PyObject *name; PyObject *age; PyObject *gender; }; static PyObject *MyClass_new (PyTypeObject *cls, PyObject *args, PyObject *kw) { MyClass *self = (MyClass *)cls -> tp_alloc(cls, 0 ); return (PyObject *)self; } static int MyClass_init (PyObject *self, PyObject *args, PyObject *kw) { PyObject *name; PyObject *age = NULL ; PyObject *gender = NULL ; char *keys[] = {"name" , "age" , "gender" , NULL }; if (!PyArg_ParseTupleAndKeywords(args, kw, "O!|O!O!" , keys, &PyUnicode_Type, &name, &PyLong_Type, &age, &PyUnicode_Type, &gender)){ return -1 ; } Py_XINCREF(name); if (age) Py_XINCREF(age); else age = PyLong_FromLong(17 ); if (gender) Py_XINCREF(gender); else gender = PyUnicode_FromWideChar(L"萌妹子" , 3 ); ((MyClass *)self) -> name = name; ((MyClass *)self) -> age = age; ((MyClass *)self) -> gender = gender; return 0 ; } void MyClass_del (PyObject *self) { Py_XDECREF(((MyClass *)self) -> name); Py_XDECREF(((MyClass *)self) -> age); Py_XDECREF(((MyClass *)self) -> gender); Py_TYPE(self) -> tp_free(self); } static PyObject *age_incr_1 (PyObject *self, PyObject *args, PyObject *kw) { ((MyClass *)self) -> age = PyNumber_Add(((MyClass *)self) -> age, PyLong_FromLong(1 )); return Py_None; } static PyMethodDef MyClass_methods[] = { {"age_incr_1" , (PyCFunction)age_incr_1, METH_VARARGS | METH_KEYWORDS, "method age_incr_1" }, {NULL , NULL , 0 , NULL } }; static PyModuleDef module = { PyModuleDef_HEAD_INIT, "kagura_nana" , "this is a module named hanser" , -1 , 0 , NULL , NULL , NULL , NULL }; PyMODINIT_FUNC PyInit_kagura_nana (void ) { static PyTypeObject cls; PyVarObject ob_base = {1 , &PyType_Type, 0 }; cls.ob_base = ob_base; cls.tp_name = "MyClass" ; cls.tp_basicsize = sizeof (MyClass); cls.tp_itemsize = 0 ; cls.tp_new = MyClass_new; cls.tp_init = MyClass_init; cls.tp_dealloc = MyClass_del; static PyMemberDef members[] = { { "name" , T_OBJECT_EX, offsetof(MyClass, name), 0 , "this is a name" }, {"age" , T_OBJECT_EX, offsetof(MyClass, age), 0 , "this is a age" }, {"gender" , T_OBJECT_EX, offsetof(MyClass, gender), 0 , "this is a gender" }, {NULL } }; cls.tp_members = members; cls.tp_methods = MyClass_methods; if (PyType_Ready(&cls) < 0 ){ return NULL ; } Py_XINCREF(&cls); PyObject *m = PyModule_Create(&module); PyModule_AddObject(m, "MyClass" , (PyObject *)&cls); return m; }
我们看到几乎没有任何区别,那么下面就来测试一下:
1 2 3 4 5 6 >>> import kagura_nana>>> self = kagura_nana.MyClass("古明地恋" , 16 , "美少女" )>>> self.age_incr_1()>>> self.age17 >>>
循环引用造成的内存泄漏 我们说 Python 的引用计数有一个重大缺陷,那就是它无法解决循环引用。
1 2 3 while True : my = MyClass("古明地觉" ) my.name = my
如果你执行上面这段代码的话,那么你会发现内存不断飙升,很明显我们上面在 C 中定义的类是没有考虑循环引用的,因为它没有被 GC 跟踪。
我们看到由于内存使用量不断增加,最后被操作系统强制 kill 掉了,主要就在于我们没有解决循环引用,导致实例对象不断被创建、但却没有被回收(引用计数最大的缺陷)。如果想要解决循环引用的话,那么就需要 Python 中的 GC 出马,而使用 GC 的前提是这个类的实例对象要被 GC 跟踪,因此我们还需要指定 tp_flags。除此之外,我们还要指定 tp_traverse(判断内部成员是否被循环引用)和 tp_clear(清理)两个函数,至于具体细节编写代码时有所体现。最后我们上面的那个类也是不允许被继承的,如果想被继承,同样需要指定 tp_flags。
1 2 3 4 5 6 7 8 >>> import kagura_nana>>> class A (kagura_nana.MyClass):... pass ... Traceback (most recent call last): File "<stdin>" , line 1 , in <module> TypeError: type 'MyClass' is not an acceptable base type >>>
我们看到 MyClass 不是一个可以被继承的类,那么下面我们来进行修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 #include "Python.h" #include "structmember.h" class MyClass { public: PyObject_HEAD PyObject *name; PyObject *age; PyObject *gender; }; static PyObject *MyClass_new (PyTypeObject *cls, PyObject *args, PyObject *kw) { MyClass *self = (MyClass *)cls -> tp_alloc(cls, 0 ); return (PyObject *)self; } static int MyClass_init (PyObject *self, PyObject *args, PyObject *kw) { PyObject *name; PyObject *age = NULL ; PyObject *gender = NULL ; char *keys[] = {"name" , "age" , "gender" , NULL }; if (!PyArg_ParseTupleAndKeywords(args, kw, "O!|O!O!" , keys, &PyUnicode_Type, &name, &PyLong_Type, &age, &PyUnicode_Type, &gender)){ return -1 ; } Py_XINCREF(name); if (age) Py_XINCREF(age); else age = PyLong_FromLong(17 ); if (gender) Py_XINCREF(gender); else gender = PyUnicode_FromWideChar(L"萌妹子" , 3 ); ((MyClass *)self) -> name = name; ((MyClass *)self) -> age = age; ((MyClass *)self) -> gender = gender; return 0 ; } static PyObject *age_incr_1 (PyObject *self, PyObject *args, PyObject *kw) { ((MyClass *)self) -> age = PyNumber_Add(((MyClass *)self) -> age, PyLong_FromLong(1 )); return Py_None; } static PyMethodDef MyClass_methods[] = { {"age_incr_1" , (PyCFunction)age_incr_1, METH_VARARGS | METH_KEYWORDS, "method age_incr_1" }, {NULL , NULL , 0 , NULL } }; static int MyClass_traverse (MyClass *self, visitproc visit, void *arg) { Py_VISIT(self -> name); Py_VISIT(self -> age); Py_VISIT(self -> gender); return 0 ; } static int MyClass_clear (MyClass *self) { Py_CLEAR(self -> name); Py_CLEAR(self -> age); Py_CLEAR(self -> gender); return 0 ; } void MyClass_del (PyObject *self) { MyClass_clear((MyClass *) self); PyObject_GC_UnTrack(self); Py_TYPE(self) -> tp_free(self); } static PyModuleDef module = { PyModuleDef_HEAD_INIT, "kagura_nana" , "this is a module named hanser" , -1 , 0 , NULL , NULL , NULL , NULL }; PyMODINIT_FUNC PyInit_kagura_nana (void ) { static PyTypeObject cls; PyVarObject ob_base = {1 , &PyType_Type, 0 }; cls.ob_base = ob_base; cls.tp_name = "MyClass" ; cls.tp_basicsize = sizeof (MyClass); cls.tp_itemsize = 0 ; cls.tp_new = MyClass_new; cls.tp_init = MyClass_init; cls.tp_dealloc = MyClass_del; static PyMemberDef members[] = { { "name" , T_OBJECT_EX, offsetof(MyClass, name), 0 , "this is a name" }, {"age" , T_OBJECT_EX, offsetof(MyClass, age), 0 , "this is a age" }, {"gender" , T_OBJECT_EX, offsetof(MyClass, gender), 0 , "this is a gender" }, {NULL } }; cls.tp_members = members; cls.tp_methods = MyClass_methods; cls.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC; cls.tp_traverse = (traverseproc) MyClass_traverse; cls.tp_clear = (inquiry) MyClass_clear; if (PyType_Ready(&cls) < 0 ){ return NULL ; } Py_XINCREF(&cls); PyObject *m = PyModule_Create(&module); PyModule_AddObject(m, "MyClass" , (PyObject *)&cls); return m; }
下面我们来继续测试一下,看看有没有问题:
可以看到,此时类可以被继承了,并且也没有出现循环引用导致的内存泄漏。
真的想说,用 C 写扩展实在是太不容易了,很明显这还只是非常简单的,因为目前这个类基本没啥方法。如果加上描述符、自定义迭代器,或者我们再多写几个方法。方法之间互相调用,导入模块(目前还没有说)等等,绝对是让人头皮发麻的事情,所以写扩展我一般只用 Cython。
全局解释器锁 我们使用 C / C++ 写扩展除了增加效率之外,最大的特点就是能够释放掉 GIL,关于 GIL 也是一个老生常谈的问题。我在前面系列已经说过,这里不再赘述了。
那么问题来了,在 C 中如何获取 GIL 呢?
1 2 3 4 5 6 7 8 9 10 11 Py_GILState_STATE gstate; gstate = PyGILState_Ensure(); call_some_function(); PyGILState_Release(gstate);
一旦在 C 中获取到 GIL,那么 Python 的其它线程都必须处于等待状态,并且当调用扩展模块中的函数时,解释器是没有权利迫使当前线程释放 GIL 的,因为调用的是 C 的代码,Python 解释器能控制的只有 Python 的字节码这一层。所以在一些操作执行结束后,必须要主动释放 GIL,否则 Python 的其它线程永远不会得到被调度的机会。
但有时我们做的是一些纯 C / C++ 操作,不需要和 Python 进行交互,这个时候希望告诉 Python 解释器,其它的线程该执行执行,不用等我,这个时候怎么做呢?首先Python 底层给我们提供了两个宏:Py_BEGIN_ALLOW_THREADS 和 Py_END_ALLOW_THREADS。
1 2 3 4 5 6 7 8 #define Py_BEGIN_ALLOW_THREADS { \ PyThreadState *_save; \ _save = PyEval_SaveThread(); #define Py_END_ALLOW_THREADS PyEval_RestoreThread(_save); \ }
从宏定义中我们可以看出,这两个宏是需要成对出现的,当然你也可以使用更细的 API 自己控制。总之:当释放 GIL 的时候,一定不要和 Python 进行交互,或者说不能有任何 Python / C API 的调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 #include "Python.h" #include <pthread.h> void * test (void *lst) { PyGILState_STATE gstate; gstate = PyGILState_Ensure(); PyObject *lst1 = (PyObject *) lst; PyObject *item = PyLong_FromLong(123 ); PyList_Append(lst1, item); Py_XDECREF(item); item = PyUnicode_FromString("hello matsuri" ); PyList_Append(lst1, item); Py_XDECREF(item); return NULL ; } static PyObject* test_gil (PyObject *self, PyObject *args) { PyObject *lst = NULL ; if (!PyArg_ParseTuple(args, "O!" , &PyList_Type, &lst)){ return NULL ; } pthread_t tid; int res = pthread_create(&tid, NULL , test, (void *)lst); if (res != 0 ) { printf ("pthread_create error: error_code = %d\n" , res); } return Py_None; } static PyMethodDef methods[] = { {"test_gil" , (PyCFunction) test_gil, METH_VARARGS, "this is a function named test_gil" }, {NULL , NULL , 0 , NULL } }; static PyModuleDef module = { PyModuleDef_HEAD_INIT, "kagura_nana" , "this is a module named hanser" , -1 , methods, NULL , NULL , NULL , NULL }; PyMODINIT_FUNC PyInit_kagura_nana (void ) { PyObject *m = PyModule_Create(&module); return m; }
我们来测试一下:
我们看了程序就无法执行了,因为 Python 只能利用单核,我们在 C 中开启了子线程,然后创建对应的 Python 线程。此时就有两个 Python 线程,只不过一个是主线程,另一个是在 C 中创建的子线程,然后这个子线程通过 Python / C API 获取了 GIL,但是用完了不释放,这就导致了主线程永远得不到机会执行。当然也无法接收 Ctrl + C 命令,因此我们需要新启一个终端 kill 掉它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #include "Python.h" #include <pthread.h> void * test (void *lst) { PyGILState_STATE gstate; gstate = PyGILState_Ensure(); PyObject *lst1 = (PyObject *) lst; PyObject *item = PyLong_FromLong(123 ); PyList_Append(lst1, item); Py_XDECREF(item); item = PyUnicode_FromString("hello matsuri" ); PyList_Append(lst1, item); Py_XDECREF(item); PyGILState_Release(gstate); return NULL ; } static PyObject* test_gil (PyObject *self, PyObject *args) { PyObject *lst = NULL ; if (!PyArg_ParseTuple(args, "O!" , &PyList_Type, &lst)){ return NULL ; } pthread_t tid; int res = pthread_create(&tid, NULL , test, (void *)lst); if (res != 0 ) { printf ("pthread_create error: error_code = %d\n" , res); } return Py_None; } static PyMethodDef methods[] = { {"test_gil" , (PyCFunction) test_gil, METH_VARARGS, "this is a function named test_gil" }, {NULL , NULL , 0 , NULL } }; static PyModuleDef module = { PyModuleDef_HEAD_INIT, "kagura_nana" , "this is a module named hanser" , -1 , methods, NULL , NULL , NULL , NULL }; PyMODINIT_FUNC PyInit_kagura_nana (void ) { PyObject *m = PyModule_Create(&module); return m; }
然后我们再来测试一下:
我们看到此时就没有任何问题了,当 C 中的线程将 GIL 给释放掉之后,此时它和 Python 线程就没有关系了,它就是 C 的线程。那么下面可以写纯 C / C++ 代码,此时可以实现并行执行。但是能不用多线程就不用多线程,因为多线程出现 bug 之后难以调试。
另外我们目前是在 C 中创建的 Python 线程,但是很明显这需要你对 C 的多线程理解有一定要求。那么我也可以不在 C 中创建,而是在 Python 中创建子线程去调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include "Python.h" static PyObject* test_gil (PyObject *self, PyObject *args) { PyObject *lst = NULL ; if (!PyArg_ParseTuple(args, "O!" , &PyList_Type, &lst)){ return NULL ; } Py_BEGIN_ALLOW_THREADS long a; while (1 ) a ++; Py_END_ALLOW_THREADS return Py_None; } static PyMethodDef methods[] = { {"test_gil" , (PyCFunction) test_gil, METH_VARARGS, "this is a function named test_gil" }, {NULL , NULL , 0 , NULL } }; static PyModuleDef module = { PyModuleDef_HEAD_INIT, "kagura_nana" , "this is a module named hanser" , -1 , methods, NULL , NULL , NULL , NULL }; PyMODINIT_FUNC PyInit_kagura_nana (void ) { PyObject *m = PyModule_Create(&module); return m; }
然后我们在 Python 中创建子线程去调用:
我们开启了一个子线程,去调用扩展模块中的函数,然后主线程也写了一个死循环。下面看一下 CPU 的使用率:
我们看到成功利用了多核,此时我们就通过编写扩展的方式来绕过了解释器中 GIL 的限制。
所以对于一些 C / C++ 逻辑,它们不需要和 Python 进行所谓的交互,那么我们就可以把 GIL 释放掉。因为 GIL 本来就是为了保护 Python 中的对象的,为了内存管理,CPython 的开发人员为了直接在解释器上面加上了一把超级大锁,但是当我们不需要和 Python 对象进行交互的时候,就可以把 GIL 给释放掉。
GIL 是字节码级别互斥锁,当线程执行字节码的时候,如果自身已经获取到 GIL ,那么会判断是否有释放的 GIL 的请求(gil_drop_request):有则释放、将 CPU 使用权交给其它线程,没有则直接执行字节码;如果自身没有获取到 GIL,那么会先判断 GIL 是否被别的线程获取,若被别的线程获取就一直申请、没有则拿到 GIL 执行字节码。
总结 这一次我们聊了聊 Python 和 C/C++ 联合编程,我们可以在 Python 中引入 C/C++,也可以在 C/C++ 中引入 Python,甚至还可以定制 Python 解释器。只不过笔者是主 Python 的,因此在 C/C++ 中引入 Python 就不说了。
Python 引入 C/C++ 主要是通过编写扩展的方式,这真的是一件痛苦的事情,需要你对 Python / C API 有很深的了解,最后仍然安利一波 Cython。