博学的乌冬面 · 了解国产大功率交流伺服电机价格——首选性价比 ...· 1 月前 · |
逆袭的柿子 · 曲莹璞:以数字技术赋能中华文化传播创新 - ...· 2 月前 · |
打酱油的荒野 · Python for循环嵌套如何优化 – ...· 3 月前 · |
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 进行编译。
通过 ctypes 模块(Python 自带的)调用 C / C++ 动态库,也算是 Python 和 C / C++ 联合编程的一种方案,而且是最简单的一种方案。因为它只对你的操作系统有要求,比如 Windows 上编译的动态库是 .dll 文件,Linux 上编译的动态库是 .so 文件,只要操作系统一致,那么任何提供了 ctypes 模块的 Python 解释器都可以调用。这种方式的使用场景是 Python 和 C / C++ 不需要做太多的交互,比如嵌入式设备,可能只是简单调用底层驱动提供的某个接口而已。
再比如我们使用 C / C++ 写了一个高性能的算法,然后通过 Python 的 ctypes 模块进行调用也是可以的,但我们之前说使用 ctypes 具有相应的局限性,这个局限性就是 C / C++ 提供的接口不能太复杂。因为 ctypes 提供的交互能力还是比较有限的,最明显的问题就是不同语言数据类型不同,一些复杂的交互方式还是比较难做到的,还有多线程的控制问题等等。
首先我们来举个栗子,演示一下。
int f(){
return 123;
这是个简单到不能再简单的 C 函数,然后我们来编译成动态库。
编译方式: gcc -o .dll文件或者.so文件 -shared c或者c++源文件
如果你用的是 Visual Studio,那么把 gcc 换成 cl 即可。我当前的源文件叫做 main.c,我们编译成 main.dll,那么命令就需要这么写:gcc -o main.dll -shared main.c。
编译成功之后,我们通过 ctypes 来进行调用。
import ctypes
# 使用 ctypes 很简单,直接import进来,然后使用 ctypes.CDLL 这个类来加载动态链接库
# 或者是用 ctypes.cdll.LoadLibrary("./main.dll")
lib = ctypes.CDLL(r"./main.dll") # 加载之后就得到了动态链接库对象
# 我们可以直接通过 . 的方式去调用里面的函数了,会发现成功打印
print(lib.f()) # 123
# 但是为了确定是否存在这个函数,我们一般会使用反射去获取
# 因为如果函数不存在通过 . 的方式调用会抛异常的
func = getattr(lib, "f", None)
if func:
print(func) # <_FuncPtr object at 0x0000029F75F315F0>
func() # hello world
# 不存在 f2 这个函数,所以得到的结果为 None
func1 = getattr(lib, "f2", None)
print(func1) # None
所以使用ctypes去调用动态链接库非常方便,过程很简单:
1. 通过 ctypes.CDLL 去加载动态库,另外注意的是:dll 或者 so 文件的路径最好是绝对路径,即便不是也要表明层级。比如我们这里的 py 文件和 dll 文件是在同一个目录下,但是我们加载的时候不可以写 main.dll,这样会报错找不到,我们需要写成 ./main.dll
2. 加载动态链接库之后会返回一个对象,我们上面起名为 lib,这个 lib 就是得到的动态链接库了
3. 然后可以直接通过 lib 调用里面的函数,但是一般我们会使用反射的方式来获取,因为不知道函数到底存不存在,如果不存在直接调用会抛出异常,如果存在这个函数我们才会调用。
Linux 和 Mac 也是一样的,这里不演示了,只不过编译之后的名字不一样。Linux 系统是 .so,Mac 系统是 .dylib。
此外我们也可以在 C 中进行打印,举个栗子:
#include <stdio.h>
void f(){
printf("hello world");
然后编译,进行调用。
import ctypes
lib = ctypes.CDLL(r"./main.dll") # 加载之后就得到了动态链接库对象
lib.f() # hello world
另外,Python 的 ctypes 调用的都是 C 语言函数,如果你用的 C++ 编译器,那么会编译成 C++ 中的函数。我们知道 C 语言的函数不支持重载,说白了就是不可以定义两个同名的函数,而 C++ 的函数是支持重载的,只要参数类型不一致即可,然后调用的时候会根据传递的参数调用对应的函数。所以当我们使用 C++ 编译器的时候,需要通过 extern "C" 将函数包起来,这样 C++ 编译器在编译的时候会将其编译成 C 的函数。
#include <stdio.h>
// 注意: 我们不能直接通过 extern "C" {} 将函数包起来, 因为这不符合 C 的语法, extern 在 C 中是用来声明一个外部变量的
// 所以我们应该使用宏替换的方式, 如果是 C++ 编译器的话, 那么编译的时候 #ifdef __cplusplus 是会通过的, 因为 __cplusplus 是一个预定义的宏
// 如果是 C 编译器, 那么 #ifdef __cplusplus 不会通过
#ifdef __cplusplus
extern "C" {
#endif
void f() {
printf("hello world\n");
#ifdef __cplusplus
#endif
当然我们在介绍 ctypes 使用的 gcc 都是 C 编译器,会编译成 C 的函数,所以后面 extern "C" 的逻辑就不加了。
我们以上就演示了,如何通过 Python 的 ctypes 模块来调用 C / C++ 动态库,但显然目前还是远远不够的。比如说:
double f() {
return 3.14;
然后我们调用的时候,会得到什么结果呢?来试一下:
import ctypes
lib = ctypes.CDLL(r"./main.dll") # 加载之后就得到了动态链接库对象
print(lib.f()) # 1374389535
我们看到得到一个不符合预期的结果,我们暂且不纠结它是怎么来的,现在的问题是它返回的为什么不是 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
下面来演示一下:
import ctypes
# 下面都是 ctypes 中提供的类,将 Python 中的对象传进去,就可以转换为 C 语言能够识别的类型
print(ctypes.c_int(1)) # c_long(1)
print(ctypes.c_uint(1)) # c_ulong(1)
print(ctypes.c_short(1)) # c_short(1)
print(ctypes.c_ushort(1)) # c_ushort(1)
print(ctypes.c_long(1)) # c_long(1)
print(ctypes.c_ulong(1)) # c_ulong(1)
# c_longlong 等价于 c_long,c_ulonglong 等价于c_ulong
print(ctypes.c_longlong(1)) # c_longlong(1)
print(ctypes.c_ulonglong(1)) # c_ulonglong(1)
print(ctypes.c_float(1.1)) # c_float(1.100000023841858)
print(ctypes.c_double(1.1)) # c_double(1.1)
# 在64位机器上,c_longdouble等于c_double
print(ctypes.c_longdouble(1.1)) # c_double(1.1)
print(ctypes.c_bool(True)) # c_bool(True)
# 相当于c_longlong和c_ulonglong
print(ctypes.c_ssize_t(10)) # c_longlong(10)
print(ctypes.c_size_t(10)) # c_ulonglong(10)
字符类型转换、指针类型转换
C 语言的字符类型分为如下:
char:一个 ascii 字符或者 -128~127 的整型
wchar:一个 unicode 字符
unsigned char:一个 ascii 字符或者 0~255 的一个整型
C 语言的指针类型分为如下:
char *:字符指针
wchar_t *:字符指针
void *:空指针
import ctypes
# 必须传递一个字节(里面是 ascii 字符),或者一个 int,来代表 C 里面的字符
print(ctypes.c_char(b"a")) # c_char(b'a')
print(ctypes.c_char(97)) # c_char(b'a')
# 传递一个 unicode 字符,当然 ascii 字符也是可以的,并且不是字节形式
print(ctypes.c_wchar("憨")) # c_wchar('憨')
# 和 c_char 类似,但是 c_char 既可以传入单个字节、也可以传整型,而这里的 c_byte 则要求必须传递整型。
print(ctypes.c_byte(97)) # c_byte(97)
print(ctypes.c_ubyte(97)) # c_ubyte(97)
# c_char_p 就是 c 里面字符数组了,其实我们可以把它看成是 Python 中的 bytes 对象
# char *s = "hello world";
# 那么这里面也要传递一个 bytes 类型的字符串,返回一个地址
print(ctypes.c_char_p(b"hello world")) # c_char_p(2082736374464)
# 直接传递一个字符串,同样返回一个地址
print(ctypes.c_wchar_p("憨八嘎~")) # c_wchar_p(2884583039392)
# ctypes.c_void_p后面演示
常见的类型就是上面这些,至于其他的类型,比如整型指针、数组、结构体、回调函数等等,ctypes 也是支持的,我们后面会介绍。
下面我们来看看如何传递参数。
#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。
from ctypes import *
lib = CDLL(r"./main.dll") # 加载之后就得到了动态链接库对象
lib.test(1, 1.2, b"hello world")
except Exception as e:
print(e) # argument 2: <class 'TypeError'>: Don't know how to convert parameter 2
# 我们看到一个问题,那就是报错了,告诉我们不知道如何转化第二个参数
# 正如我们之前说的,整型是会自动转化的,但是浮点型是不会自动转化的
# 因此我们需要使用 ctypes 来包装一下,当然还有整型,即便整型会自动转,我们还是建议手动转化一下
# 这里传入 c_int(1) 和 1 都是一样的,但是建议传入 c_int(1)
lib.test(c_int(1), c_float(1.2), c_char_p(b"hello world")) # a = 1, b = 1.20, s = hello world
我们看到完美的打印出来了,我们再来试试布尔类型。
#include <stdio.h>
void test(_Bool flag)
//布尔类型本质上是一个int
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
然后再来看看字符和字符数组的传递:
#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");
printf("age >= 18, gender is male\n");
if (strcmp(gender, "female") == 0)
printf("age < 18, gender is female\n");
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 中是可以被修改的。
#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 语言传递可以修改字符的空间。
from ctypes import *
# 传入一个 int,表示创建一个具有固定大小的字符缓存,这里是 10 个
s = create_string_buffer(10)
# 直接打印就是一个对象
print(s) # <ctypes.c_char_Array_10 object at 0x000001E2E07667C0>
# 也可以调用 value 方法打印它的值,可以看到什么都没有
print(s.value) # b''
# 并且它还有一个 raw 方法,表示 C 语言中的字符数组,由于长度为 10,并且没有内容,所以全部是 \x00,就是C语言中的 \0
print(s.raw) # b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
# 还可以查看长度
print(len(s)) # 10
# 其它类型也是一样的
v = c_int(1)
# 我们看到 c_int(1) 它的类型就是 ctypes.c_long
print(type(v)) # <class 'ctypes.c_long'>
# 当然你把 c_int,c_long,c_longlong 这些花里胡哨的都当成是整型就完事了
# 此外我们还能够拿到它的值,调用 value 方法
print(v.value, type(v.value)) # 1 <class 'int'>
v = c_char(b"a")
print(type(v)) # <class 'ctypes.c_char'>
print(v.value, type(v.value)) # b'a' <class 'bytes'>
v = c_char_p(b"hello world")
print(type(v)) # <class 'ctypes.c_char_p'>
print(v.value, type(v.value)) # b'hello world' <class 'bytes'>
v = c_wchar_p("夏色祭")
print(type(v)) # <class 'ctypes.c_wchar_p'>
print(v.value, type(v.value)) # 夏色祭 <class 'str'>
# 因此 ctypes 中的对象调用 value 即可得到 Python 中的对象
当然 create_string_buffer 如果只传一个 int,那么表示创建对应长度的字符缓存。除此之外,还可以指定字节串,此时的字符缓存大小和指定的字节串大小是一致的:
from ctypes import *
# 此时我们直接创建了一个字符缓存
s = create_string_buffer(b"hello")
print(s) # <ctypes.c_char_Array_6 object at 0x0000021944E467C0>
print(s.value) # b'hello'
# 我们知道在 C 中,字符数组是以 \0 作为结束标记的,所以结尾会有一个 \0,因为 raw 表示 C 中原始的字符数组
print(s.raw) # b'hello\x00'
# 长度为 6,b"hello" 五个字符再加上 \0 一共 6 个
print(len(s))
当然 create_string_buffer 还可以在指定字节串的同时,指定空间大小。
from ctypes import *
# 此时我们直接创建了一个字符缓存,如果不指定容量,那么默认和对应的字符数组大小一致
# 但是我们还可以同时指定容量,记得容量要比前面的字节串的长度要大。
s = create_string_buffer(b"hello", 10)
print(s) # <ctypes.c_char_Array_10 object at 0x0000019361C067C0>
print(s.value) # b'hello'
# 长度为 10,剩余的 5 个显然是 \0
print(s.raw) # b'hello\x00\x00\x00\x00\x00'
print(len(s)) # 10
下面我们来看看如何使用 create_string_buffer 来传递:
#include <stdio.h>
int test(char *s)
//变量的形式依旧是char *s
//下面的操作就是相当于把字符数组的索引为5到11的部分换成" satori"
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
就表示这个字符数组结束了。
from ctypes import *
lib = CDLL("./main.dll")
# 这里把"hello"换成"hell",看看会发生什么
s = create_string_buffer(b"hell", 20)
lib.test(s) # s = hell
# 我们看到这里只打印了"hell",这是为什么?
# 我们看一下这个s
print(s.raw) # b'hell\x00 satori\x00\x00\x00\x00\x00\x00\x00\x00'
# 我们看到这个 create_string_buffer 返回的对象是可变的,在将 s 传进去之后被修改了
# 如果没有传递的话,我们知道它是长这样的。
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 去调用。
from ctypes import *
import sys
import platform
# 判断当前的操作系统平台。
# Windows 平台返回 "Windows",Linux 平台返回 "Linux",MacOS 平台返回 "Darwin"
system = 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)
# 调用对应的函数,比如 printf,注意里面需要传入字节
lib.printf(b"my name is %s, age is %d\n", b"van", 37) # my name is van, age is 37
# 如果包含汉字就不能使用 b"" 这种形式了,因为这种形式只适用于 ascii 字符,我们需要手动 encode 成 utf-8
lib.printf("姓名: %s, 年龄: %d\n".encode("utf-8"), "古明地觉".encode("utf-8"), 17) # 姓名: 古明地觉, 年龄: 17
我们上面是在 Windows 上调用的,这段代码即便拿到 Linux 和 MacOS 上也可以正常执行。
当然这里面还支持其他的函数,我们这里以 Windows 为例:
from ctypes import *
libc = cdll.msvcrt
# 创建一个大小为 10 的buffer
s = create_string_buffer(10)
# strcpy 表示将字符串进行拷贝
libc.strcpy(s, c_char_p(b"hello satori"))
# 由于 buffer 只有10个字节大小,所以无法完全拷贝
print(s.value) # b'hello sato'
# 创建 unicode buffer
s = create_unicode_buffer(10)
libc.strcpy(s, c_wchar_p("我也觉得很变态啊"))
print(s.value) # 我也觉得很变态啊
# 比如 puts 函数
libc.puts(b"hello world") # hello world
对于 Windows 来说,我们还可以调用一些其它的函数,但是不再是通过 cdll.msvcrt 这种方式了。在 Windows 上面有一个 user32 这么个东西,我们来看一下:
from ctypes import *
# 我们通过 cdll.user32 本质上还是加载了 Windows 上的一个共享库
# 这个库给我们提供了很多方便的功能
win = cdll.user32
# 比如查看屏幕的分辨率
print(win.GetSystemMetrics(0)) # 1920
print(win.GetSystemMetrics(1)) # 1080
我们还可以用它来打开 MessageBoxA:
可以看到我们通过 cdll.user32 就可以很轻松地调用 Windows 的 api,具体有哪些 api 可以去网上查找,搜索 win32 api 即可。
除了 ctypes,还有几个专门用来操作 win32 服务的模块,win32gui、win32con、win32api、win32com、win32process。直接 pip install pywin32 即可,或者 pip install pypiwin32。
显示窗体和隐藏窗体
import win32gui
import win32con
# 首先查找窗体,这里查找 qq。需要传入 窗口类名 窗口标题名,至于这个怎么获取可以使用 spy 工具查看
qq = win32gui.FindWindow("TXGuifoundation", "QQ")
# 然后让窗体显示出来
win32gui.ShowWindow(qq, win32con.SW_SHOW)
# 还可以隐藏
win32gui.ShowWindow(qq, win32con.SW_HIDE)
控制窗体的位置和大小
import win32gui
import win32con
qq = win32gui.FindWindow("TXGuiFoundation", "QQ")
# 主要要接收如下参数
# 参数一:控制的窗体
# 参数二:大致方位:HWND_TOPMOST,位于上方
# 参数三:位置x
# 参数四:位置y
# 参数五:长度
# 参数六:宽度
# 参数七:比较固定,就是让窗体一直显示
win32gui.SetWindowPos(qq, win32con.HWND_TOPMOST, 100, 100, 300, 300, win32con.SWP_SHOWWINDOW)
那么我们还可以让窗体满屏幕乱跑:
import win32gui
import win32con
import random
qqWin = 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)
import win32com.client
# 直接调用操作系统的语音接口
speaker = win32com.client.Dispatch("SAPI.SpVoice")
# 输入你想要说的话,前提是操作系统语音助手要认识。一般中文和英文是没有问题的
speaker.Speak("他能秒我,他能秒杀我?他要是能把我秒了,我当场······")
Python 中 win32 模块的 api 非常多,几乎可以操作整个 Windows 提供的服务,win32 模块就是相当于把 Windows 服务封装成了一个一个的接口。不过这些服务、或者调用这些服务具体都能干些什么,可以自己去研究,这里就到此为止了。
ctypes 获取返回值
我们前面已经看到了,通过 ctypes 向动态链接库中的函数传参时是没有问题的,但是我们如何拿到返回值呢?我们之前都是使用 printf 直接打印的,但是这样显然不行,我们肯定是要拿到返回值去做一些别的事情的。那么我们在 C 函数中直接 return 不就可以啦,还记得之前演示的返回浮点型的例子吗?我们明明返回了 3.14,但得到的确是一大长串整数,所以我们需要在调用函数之前告诉 ctypes 返回值的类型。
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 指定一下返回值到底是什么类型,只有这样才能拿到动态链接库中函数的正确的返回值。
#include <wchar.h>
char * test1()
char *s = "hello satori";
return s;
wchar_t * test2()
// 遇到 wchar_t 的时候,一定要导入 wchar.h 头文件
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 调用动态链接库的函数返回的都是整型,至于返回的整型的值到底是什么?从哪里来的?我们不需要关心,你可以理解为地址、或者某块内存的脏数据,但是不管怎么样,结果肯定是不正确的(如果函数返回的就是整形除外)。因此我们需要提前声明一下返回值的类型。声明方式:
lib.CFunction.restype = ctypes类型
我们说 lib 就是 ctypes 调用 dll 或者 so 得到的动态链接库,而里面的函数就相当于是一个个的 CFunction,然后设置内部的 restype(返回值类型),就可以得到正确的返回值了。另外即便返回值设置的不对,比如:test1 返回一个 char *,但是我们将类型设置为 c_float,调用的时候也不会报错而且得到的也是一个 float,但是这个结果肯定是不对的。
from ctypes import *
lib = CDLL("./main.dll")
lib.test1.restype = c_char_p
print(lib.test1()) # b'hello satori'
# 设置为 c_float
lib.test1.restype = c_float
# 获取了不知道从哪里来的脏数据
print(lib.test1()) # 2.5420596244190436e+20
# 另外 ctypes 调用还有一个特点
lib.test2.restype = c_wchar_p
print(lib.test2(123, c_float(1.35), c_wchar_p("呼呼呼"))) # 憨八嘎
# 我们看到 test2 是不需要参数的,如果我们传了那么就会忽略掉,依旧能得到正常的返回值
# 但是不要这么做,因为没准就出问题了,所以还是该传几个参数就传几个参数
下面我们来看看浮点类型的返回值怎么获取,当然方法和上面是一样的。
#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 创建指针
from ctypes import *
v = c_int(123)
# 我们知道可以通过 value 属性获取相应的值
print(v.value)
# 但是我们还可以修改
v.value = 456
print(v) # c_long(456)
s = create_string_buffer(b"hello")
s[3] = b'>'
print(s.value) # b'hel>o'
# 如何创建指针呢?通过 byref 和 pointer
v2 = c_int(123)
print(byref(v2)) # <cparam 'P' (000001D9DCF86888)>
print(pointer(v2)) # <__main__.LP_c_long object at 0x000001D9DCF868C0>
我们看到 byref 和 pointer 都可以创建指针,那么这两者有什么区别呢?byref 返回的指针相当于右值,而 pointer 返回的指针相当于左值。举个栗子:
// 以整型的指针为例:
int num = 123;
int *p = &num
对于上面的例子,如果是 byref,那么结果相当于 &num,拿到的就是一个具体的值。如果是 pointer,那么结果相当于 p。这两者在传递的时候是没有区别的,只是对于 pointer 来说,它返回的是一个左值,我们是可以继续拿来做文章的。
from ctypes import *
n = c_int(123)
# 拿到变量 n 的指针
p1 = byref(n)
p2 = pointer(n)
# pointer 返回的是左值,我们可以继续做文章,比如继续获取指针,此时获取的就是 p2 的指针
print(byref(p2)) # <cparam 'P' (0000023953796888)>
# 但是 p1 不行,因为 byref 返回的是一个右值
print(byref(p1))
except Exception as e:
print(e) # byref() argument must be a ctypes instance, not 'CArgObject'
因此两者的区别就在这里,但是还是那句话,我们在传递的时候是无所谓的,传递哪一个都可以。
我们知道了可以通过 ctypes.byref、ctypes.pointer 的方式传递指针,但是如果函数返回的也是指针呢?我们知道除了返回 int 之外,都要指定返回值类型,那么指针如何指定呢?答案是通过 ctypes.POINTER。
// 接收两个 float *,返回一个 float *
float *test1(float *a, float *b)
// 因为返回指针,所以为了避免被销毁,我们使用 static 静态声明
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 则是用来获取指针的。
我们知道可以事先声明返回值的类型,这样才能拿到正确的返回值。而我们传递的时候,直接传递正确的类型即可,但是其实也是可以事先声明的。
from ctypes import *
lib = CDLL("./main.dll")
# 通过 argtypes,我们可以事先指定需要传入两个 float 的指针类型,注意:要指定为一个元组,即便是一个参数也要是元组
lib.test1.argtypes = (POINTER(c_float), POINTER(c_float))
lib.test1.restype = POINTER(c_float)
# 但是和 restype 不同,argtypes 实际上是可以不要的
# 因为返回的默认是一个整型,我们才需要通过 restype 事先声明返回值的类型,这是有必要的
# 但是对于 argtypes 来说,我们传参的时候已经直接指定类型了,所以 argtypes 即便没有也是可以的
# 所以 argtypes 的作用就类似于其他静态语言中的类型声明,先把类型定好,如果你传的类型不对,直接给你报错
# 这里第二个参数传c_int
res = lib.test1(byref(c_float(3.21)), c_int(123))
except Exception as e:
# 所以直接就给你报错了
print(e) # argument 2: <class 'TypeError'>: expected LP_c_float instance instead of c_long
# 此时正确执行
res1 = lib.test1(byref(c_float(3.21)), byref(c_float(666)))
print(res1.contents.value) # 669.2100219726562
下面我们来看看如何使用 ctypes 传递数组,这里我们只讲传递,不讲返回。因为 C 语言返回数组给 Python 实际上会存在很多问题,比如:返回的数组的内存由谁来管理,不用了之后空间由谁来释放,事实上 ctypes 内部对于返回数组支持的也不是很好。因此我们一般不会向 Python 返回一个 C 语言中的数组,因为 C 语言中的数组传递给 Python 涉及到效率的问题,Python 中的列表传递直接传递一个引用即可,但是 C 语言中的数组过来肯定是要拷贝一份的,所以这里我们只讲 Python 如何通过 ctypes 给动态链接库传递数组,不再介绍动态链接库如何返回数组给 Python。
from ctypes import *
# 创建一个数组,假设叫 [1, 2, 3, 4, 5]
a5 = (c_int * 5)(1, 2, 3, 4, 5)
print(a5) # <__main__.c_long_Array_5 object at 0x00000162428968C0>
# 上面这种方式就得到了一个数组
# 当然下面的方式也是可以的
a5 = (c_int * 5)(*range(1, 6))
print(a5) # <__main__.c_long_Array_5 object at 0x0000016242896940>
下面演示一下:
// 字符数组默认是以 \0 作为结束的,我们可以通过 strlen 来计算长度。
// 但是对于整型的数组来说我们不知道有多长
// 因此有两种声明参数的方式,一种是 int a[n],指定数组的长度
// 另一种是通过指定 int *a 的同时,再指定一个参数 int size,调用函数的时候告诉函数这个数组有多长
int test1(int a[5])
// 可能有人会问了,难道不能通过 sizeof 计算吗?答案是不能,无论是 int *a 还是 int a[n]
// 数组作为函数的参数时会退化为指针,我们调用的时候,传递的都是指针,指针在 64 位机器上默认占 8 个字节。
// 所以int a[] = {...}这种形式,如果直接在当前函数中计算的话,那么 sizeof(a) 就是数组里面所有元素的总大小,因为a是一个数组名
// 但是当把 a 传递给一个函数的时候,那么等价于将 a 的首地址拷贝一份传过去,此时在新的函数中再计算 sizeof(a) 的时候就是一个指针的大小
//至于 int *a 这种声明方式,不管在什么地方,sizeof(a) 都是一个指针的大小
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 定义一个结构体:
from ctypes import *
# 对于这样一个结构体应该如何定义呢?
struct Girl {
char *name; // 姓名
int age; // 年龄
char *gender; //性别
int class; //班级
# 定义一个类,必须继承自 ctypes.Structure
class Girl(Structure):
# 创建一个 _fields_ 变量,必须是这个名字,注意开始和结尾都只有一个下划线
# 然后就可以写结构体的字段了,具体怎么写估计一看就清晰了
_fields_ = [
("name", c_char_p),
("age", c_int),
("gender", c_char_p),
("class", c_int)
我们向 C 中传递一个结构体,然后再返回:
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
如果是结构体指针呢?
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 来获取。再举个栗子对比一下:
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) # 1 2
lib.test1.restype = Num
res = lib.test1(num)
# 我们看到通过 res 得到的结果是修改之后的值
# 但是对于 num 来说没有变
print(res.x, res.y) # 2 3
print(num.x, num.y) # 1 2
因为我们将 num 传进去之后,相当于将 num 拷贝了一份。
函数里面的结构体和这里的 num 尽管长得一样,但是没有任何关系
所以 res 获取的结果是自增之后的结果,但是 num 还是之前的 num
# 我们来试试传递指针,将 byref(num) 再传进去
lib.test2.restype = POINTER(Num)
res = lib.test2(byref(num))
print(num.x, num.y) # 2 3
我们看到将指针传进去之后,相当于把 num 的指针拷贝了一份。
然后在函数中修改,相当于修改指针指向的内存,所以是会影响外面的 num 的
而动态链接库的函数中返回的是参数中的结构体指针,而我们传递的 byref(num) 也是这里的num的指针
尽管传递指针的时候也是拷贝了一份,两个指针本身来说虽然也没有任何联系,但是它们存储的地址是一样的
那么通过 res.contents 获取到的内容就相当于是这里的 num
因此此时我们通过 res.contents 获取和通过 num 来获取都是一样的。
print(res.contents.x, res.contents.y) # 2 3
# 另外还需要注意的一点就是:如果传递的是指针,一定要先创建一个变量
# 比如这里,一定是:先要 num = Num(),然后再 byref(num),不可以直接就 byref(Num())
# 原因很简单,因为 Num() 这种形式在创建完 Num 实例之后就销毁了,因为没有变量保存它,那么此时再修改指针指向的内存就会有问题,因为内存的值已经被回收了
# 如果不是指针,那么可以直接传递 Num(),因为拷贝了一份
所以在这里,C 中返回一个指针是没有问题的,因为它指向的对象是我们在 Python 中创建的,Python 会管理它。
在看回调函数之前,我们先看看如何把一个函数赋值给一个变量。准确的说,是让一个指针指向一个函数,这个指针叫做函数指针。通常我们说的指针变量是指向一个整型、字符型或数组等等,而函数指针是指向函数。
#include <stdio.h>
int add(int a, int b){
int c;
c = a + b;
return c;
int main() {
// 创建一个指针变量 p,让 add 等于 p
// 我们看到就类似声明函数一样,指定返回值类型和变量类型即可
// 但是注意的是,中间一定是 *p,不是 p,因为这是一个函数指针,所以要有 *
int (*p)(int, int) = add;
printf("1 + 3 = %d\n", p(1, 3)); //1 + 3 = 4
return 0;
除此之外我们还以使用 typedef。
#include <stdio.h>
int add(int a, int b){
int c;
c = a + b;
return c;
// 相当于创建了一个类型,名字叫做 func,这个 func 表示的是一个函数指针类型
typedef int (*func)(int, int);
int main() {
// 声明一个 func 类型的函数指针 p,等于 add
func p = add;
printf("2 + 3 = %d\n", p(2, 3)); // 2 + 3 = 5
return 0;
下面来看看如何使用回调函数,说白了就是把一个函数指针作为函数的参数。
#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 *
char *execute1(int score, char *(*f)(int)){
return f(score);
//除了上面那种方式,我们还可以跟之前一样通过 typedef
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)); // good
printf("%s\n", execute2(70, evaluate)); // not bad
我们知道了在 C 中传入一个函数,那么在 Python 中如何定义一个 C 语言可以识别的函数呢?毫无疑问,类似于结构体,我们肯定是要先定义一个 Python 的函数,然后再把 Python 的函数转化成 C 语言可以识别的函数。
int add(int a, int b, int (*f)(int *, int *)){
return f(&a, &b);
我们就以这个函数为例,add 函数返回一个 int,接收两个 int,和一个函数指针,那么我们如何在 Python 中定义这样的函数并传递呢?
from ctypes import *
lib = CDLL("./main.dll")
# 动态链接库中的函数接收的函数的参数是两个 int *,所以我们这里的 a 和 b 也是一个 pointer
def add(a, b):
return a.contents.value + b.contents.value
# 此时我们把 C 中的函数用 Python 表达了,但是这样肯定是不可能直接传递的,能传就见鬼了
# 那我们要如何转化呢?
# 可以通过 ctypes 里面的函数 CFUNCTYPE 转化一下,这个函数接收任意个参数
# 但是第一个参数是函数的返回值类型,然后函数的参数写在后面,有多少写多少。
# 比如这里的函数返回一个 int,接收两个 int *,所以就是
t = CFUNCTYPE(c_int, POINTER(c_int), POINTER(c_int))
# 如果函数不需要返回值,那么写一个 None 即可
# 然后得到一个类型 t,此时的类型 t 就等同于 C 中的 typedef int (*t)(int*, int*);
# 将我们的函数传进去,就得到了 C 语言可以识别的函数 func
func = t(add)
# 然后调用,别忘了定义返回值类型,当然这里是 int 就无所谓了
lib.add.restype = c_int
print(lib.add(88, 96, func))
print(lib.add(59, 55, func))
print(lib.add(94, 105, func))
以上便是 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 进来的,那么它必然要有一个入口。
// 这个 xxx 非常重要,这个是你最终生成的扩展模块的名字,前面的 PyInit 是写死的
PyInit_xxx(void) // 模块初始化入口
有了入口之后,我们还需要创建模块,创建模块使用下面这个函数。
PyModule_Create // 创建模块
创建模块,那么总要有模块信息吧。
PyModuleDef // 模块信息
那么模块信息里面都可以包含哪些信息呢?模块名算吧,模块里面有哪些函数算吧。
PyMethodDef // 模块函数信息, 一个数组, 因为一个模块可以包含多个函数
而一个 Python 中的函数底层会对应一个结构体,这个结构体里面保存了 Python 函数的元信息,并且还保存了一个指向 C 函数的指针,这是显然的。
我们通过一个例子来说明以下吧,这样会更好理解一些,具体细节在编写代码的时候再补充。
def f1():
return 123
def f2(a):
return a + 1
以上是非常简单的一个模块,里面只有两个简单的函数,但是我们知道当被导入时它就是一个 PyModuleObject 对象。里面除了我们定义的两个函数之外还有其它的属性,显然这是 Python 解释器在背后帮助我们完成的,具体流程也是我们上面说的那几步(省略了亿点点细节)。
那么我们如何使用 C 来进行编写呢?下面来操作一下。
编写 Python 扩展模块,需要引入 Python.h 这个头文件
该头文件在 Python 安装目录的 include 目录下,我们必须要导入它
当然这个头文件里面还导入了很多其它的头文件,我们也可以直接拿来用
#include "Python.h"
编写我们之前的两个函数 f1 和 f2,必须返回 PyObject *
函数里面至少要接收一个 PyObject *self,而这个参数我们是不需要管的,当然不叫 self 也是可以的
显然跟方法里面的 self 是一个道理,所以对于 Python 调用者而言,f1 是一个不需要接收参数的函数
static PyObject *
f1(PyObject *self) {
return PyLong_FromLong(123);
static PyObject *
f2(PyObject *self, PyObject *a) {
long x;
// 转成 C 中的 long,进行相加,然后再转成 Python 的 int; 或者调用 PyNumber_Add() 也可以
x = PyLong_AsLong(a);
PyObject *result = PyLong_FromLong(x + 1);
return result;
// 但是注意:虽然我们定义了 f1 和 f2,但是它们是 C 中的函数,不是 Python 的
// Python 中的函数在 C 中对应的是一个结构体,里面会有函数指针,指向这里的 f1 和 f2
// 但除了函数指针,还有其它的信息
定义一个结构体数组,结构体类型为 PyMethodDef,显然这个 PyMethodDef 就是 Python 中的函数
PyMethodDef 里面有四个成员,分别是:函数名、函数指针(需要转成PyCFunction)、函数参数标识、函数的doc
关于 PyMethodDef 我们后面会单独说
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} 充当哨兵
{NULL, NULL, 0, NULL}
我们编写的 py 文件,解释器会自动把它变成一个模块,但是这里我们需要手动定义
下面定义一个 PyModuleDef 类型的结构体,它就是我们的模块信息
static PyModuleDef module = {
// 头部信息,PyModuleDef_Base m_base,正如所有对象都有 PyObject 这个结构体一样
// 而 Python.h 中提供了一个宏,#define PyModuleDef_HEAD_INIT PyModuleDef_Base m_base; 我们可以使用 PyModuleDef_HEAD_INIT 来代替
PyModuleDef_HEAD_INIT,
"kagura_nana", // 模块的名字
"this is a module named kagura_nana", // 模块的doc,没有的话直接写成NULL即可
-1, // 模块的独立空间,这个不需要关心,直接写成 -1 即可
methods, // 上面的 PyMethodDef 结构数组,必须写在这里,不然我们没法使用定义的函数
// 下面直接写4个NULL即可
NULL, NULL, NULL, NULL
// 以上便是 PyModuleDef 结构体实例的创建过程,至于里面的一些细节我们后面说
// 到目前为止,前置工作就做完了,下面还差两步
扩展库入口函数,这是一个宏,Python 的源代码我们知道是使用 C 来编写的
但是编译的时候为了支持 C++ 的编译器也能编译,于是需要通过 extern "C" 定义函数
然后这样 C++ 编译器在编译的的时候就会按照 C 的标准来编译函数,这个宏就是干这件事情的,主要和 Python 中的函数保持一致
PyMODINIT_FUNC
模块初始化入口,注意:模块名叫 kagura_nana,那么下面就必须要写成 PyInit_kagura_nana
PyInit_kagura_nana(void)
// 将 PyModuleDef 结构体实例的指针传递进去,然后返回得到 Python 中的模块
return PyModule_Create(&module);
整体逻辑还是非常简单的,过程如下:
include "Python.h",这个是必须的
定义我们函数,具体定义什么函数、里面写什么代码完全取决于你的业务
定义一个PyMethodDef结构体数组
定义一个PyModuleDef结构体
定义模块初始化入口,然后返回模块对象
那么如何将这个 C 文件变成扩展模块呢?显然要经过编译,而 Python 提供了 distutils 标准库,可以非常轻松地帮我们把 C 文件编译成扩展模块。
from distutils.core import *
setup(
# 打包之后会有一个 egg_info,表示该模块的元信息信息,name 就表示打包之后的 egg 文件名
# 显然和模块名是一致的
name="kagura_nana",
version="1.11", # 版本号
author="古明地盆",
author_email="66666@东方地灵殿.com",
# 关键来了,这里面接收一个类 Extension,类里面传入两个参数
# 第一个参数是我们的模块名,必须和 PyInit_xxx 中的 xxx 保持一致,否则报错
# 第二个参数是一个列表,表示用到了哪些 C 文件,因为扩展模块对应的 C 文件不一定只有一个,我们这里的 C 文件还叫 main.c
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,当然这都不重要,重要的是我们能不能调用,试一试吧。
import kagura_nana
print(kagura_nana) # <module 'kagura_nana' from 'C:\\python38\\lib\\site-packages\\kagura_nana.cp38-win_amd64.pyd'>
print(kagura_nana.f1()) # 123
print(kagura_nana.f2(123)) # 124
可以看到调用是没有任何问题的,最后再看一个神奇的东西,我们知道在 pycharm 这样的智能编辑器中,通过 Ctrl 加左键可以调到指定模块的指定位置。
神奇的一幕出现了,我们点击进去居然还能跳转,其实我们在编译成扩展模块移动到 site-packages
之后,pycharm 会进行检测、然后将其抽象成一个普通的 py 文件,方便你查看。我们看到模块注释、函数的注释跟我们在 C 文件中指定的一样。但是注意:该文件只是 pycharm 方便你查看函数注释等信息而专门做的一个抽象,事实上你把这个文件删掉也是没有关系的。
因此我们可以再总结一下整体流程:
第一步:include "Python.h",必须要引入这个头文件,这个头文件中还引入了 C 中的一些头文件,具体都引入了哪些库我们可以查阅。当然如果不确定但又懒得看,我们还可以手动再引入一次,反正 include 同一个头文件只会引入一次。
第二步:理论上这不是第二步,但是按照编写代码顺序我们就认为它是第二步吧,对,就是按照我们上面写的代码从上往下撸。这一步你需要编写函数,这个函数就是 C 语言中定义的函数,这个函数返回一个 PyObject * ,至少要接收一个PyObject *,我们一般叫它 self,这第一个参数你可以看成是必须的,无论我们传不传其他参数,这个参数是必需要有的。所以如果只有这一个参数,那么我们就认为这个函数不接收参数,因为我们在调用的时候没有传递。
static PyObject *
f1(PyObject *self)
static PyObject *
f2(PyObject *self)
static PyObject *
f3(PyObject *self)
// 假设我们定义了这三个函数吧,三个函数都不接受参数
第三步:定义一个 PyMethodDef 类型的数组,这个数组也是我们后面的 PyModuleDef 对象中的一个参数,这个数组名字叫什么就无所谓了。至于 PyMethodDef,我们可以单独使用 PyMethodDef 创建实例,然后将变量写到数组中,也可以直接在数组中创建。如果是直接在数组中创建的话,那么就不需要再使用 PyMethodDef 定义了,直接在 {} 里面写成员信息即可。
static PyMethodDef module_functions[] = {
// 暴露给 Python 的函数名
"f1",
// 函数指针,最好使用 PyCFunction 转一下,可以确保不出问题。
// 如果不转,我自己测试没有问题,但是编译时候会给警告,最好还是按照标准,把指针的类型转换一下
// 转换成 Python 底层识别的 PyCFunction
(PyCFunction) f1,
METH_NOARGS, // 参数类型,至于怎么接收 *args 和 **kwargs 的参数,后面说
"函数f1的注释"
{"f2", (PyCFunction)f2, METH_NOARGS, "函数f2的注释"},
{"f3", (PyCFunction)f3, METH_NOARGS, "函数f3的注释"},
//别忘记,下面的 {NULL, NULL, 0, NULL},充当哨兵
{NULL, NULL, 0, NULL}
第四步:定义 PyModuleDef 对象,这个变量的名字叫什么也没有要求。
static PyModuleDef m = {
PyModuleDef_HEAD_INIT, // 头部信息
// 模块名,这个是有讲究的,你要编译的扩展模块叫啥,这里就写啥
"kagura_nana",
"模块的注释",
-1, // 模块的空间,这个是给子解释器调用的,我们不需要关心,直接写 -1 即可,表示不使用
module_functions, // 然后是我们上面定义的数组名,里面放了一大堆的 PyMethodDef 结构体实例
// 然后是四个 NULL,因为该结构还有其它成员,但我们不需要使用,所以指定 NULL 即可。当然有的编译器比较智能,你若不指定自动为 NULL
// 但为了规范,我们还是手动写上,因为规范的做法就是给每个成员都赋上值
NULL,
NULL,
NULL,
第五步:写上一个宏,其实把它单独拆分出来,有点小题大做了。
PyMODINIT_FUNC
// 一个宏,主要是保证函数按照 C 的标准,不用在意,写上就行
第六步:创建一个模块的入口函数,我们说编译的扩展模块叫 kagura_nana,那么这个函数名就要这么写。
PyInit_kagura_nana(void)
// 会根据上面定义的 PyModuleDef 实例,得到 Python 中的模块
// PyModule_Create 就是用来创建 Python 中的模块的,直接将 PyModuleDef 定义的对象的指针扔进去
// 便可得到 Python 中的模块,然后直接返回即可。
return PyModule_Create(&m);
第七步:定义一个py文件,假设叫 xx.py,那么在里面写上如下内容,然后 python xx.py install 即可。
from distutils.core import *
setup(
# 这是生成的 egg 文件名,也是里面的元信息中的 Name
name="kagura_nana",
# 版本号
version="10.22",
author="古明地觉",
# 作者邮箱
author_email="东方地灵殿",
# 当然还有其它参数,作为元信息来描述模块,比如 description:模块介绍。
# 有兴趣的话可以看函数的注释,或者根据已有的 egg 文件自己查看
# 下面是扩展模块,Extension("yousa", ["C源文件"])
# 我们说 Extension 里面的第一个参数也必须是你的扩展模块的名字,并且必须要和 PyInit_xxx 以及 PyModuleDef 中的第一个成员保持一致
# 至于第二个参数就是一个列表,你需要用到哪些 C 源文件。
# 而且我们看到这个 Extension 也在一个列表里面,因为我们也可以传入多个 Extension 同时生成多个扩展模块。
# 我们可以写好一个生成一个,你也可以一次性写多个,然后只编译一次。
ext_modules=[Extension("hanser", ["a.c"])]
以上便是编写扩展模块的基本流程,但是里面还有很多细节没有说。
PyMethodDef
首先是 PyMethodDef,我们说它对应的是 Python 中的函数,那么我们肯定要来看看它的定义,藏身于 Include/methodobject.h 中。
struct PyMethodDef {
/* 函数名 */
const char *ml_name;
/* 实现对应逻辑的 C 函数,但是需要转成 PyCFunction 类型,主要是为了更好的处理关键字参数 */
PyCFunction ml_meth;
/* 参数类型
#define METH_VARARGS 0x0001 扩展位置参数,*args
#define METH_KEYWORDS 0x0002 扩展关键字参数,**kwargs
#define METH_NOARGS 0x0004 不需要参数
#define METH_O 0x0008 需要一个参数
#define METH_CLASS 0x0010 被 classmethod 装饰
#define METH_STATIC 0x0020 被 staticmethod 装饰
int ml_flags;
//函数的 __doc__,没有的话传递 NULL
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。
PyObject *
PyLong_FromLong(long ival)
PyLongObject *v;
// ...
return (PyObject *)v;
此外 Python 还专门定义了几个宏,来看一下:
#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 来用于引用计数的增加和减少。
// 引用计数增加很简单,就是找到 ob_refcnt,然后 ++
#define Py_INCREF(op) ( \
_Py_INC_REFTOTAL _Py_REF_DEBUG_COMMA \
((PyObject *)(op))->ob_refcnt++)
// 但是减少的话,做的事情稍微多一些
// 其实主要就是判断引用计数是否为 0,如果为 0 直接调用 _Py_Dealloc 将对象销毁
// _Py_Dealloc 也是一个宏,会调用对应类型对象的 tp_dealloc,也就是析构方法
#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 中的对象都是申请在堆区的,这个是不会自动释放的。举个栗子:
static PyObject *
f(PyObject *self)
PyObject *s = PyUnicode_FromString("你好呀~~~");
// Py_None 就是 Python 中的 None, 同理还有 Py_True、Py_False,我们后面会继续提
// 这里增加引用计数,至于为什么要增加,我们后面说
Py_INCREF(Py_None);
return Py_None;
这个函数不需要参数,如果我们写一个死循环不停的调用这个函数,你会发现内存的占用蹭蹭的往上涨。就是因为这个 PyUnicodeObject 是申请在堆区的,此时内部的引用计数为 1。函数执行完毕变量 s 被销毁了,但是 s 是一个指针,这个指针被销毁了是不假,但是它指向的内存并没有被销毁。
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 了。不过有一个特例,那就是当这个指针作为返回值的时候,我们不需要手动减去引用计数,因为会自动减。
static PyObject *
f(PyObject *self)
PyObject *s = PyUnicode_FromString("hello~~~");
// 如果我们把 s 给返回了,那么我们就不需要调用 Py_DECREF 了
// 因为一旦作为返回值,那么会自动减去 1
// 所以此时 C 中的对象是由 Python 来管理的,准确的说应该是作为返回值的指针指向的对象是由 Python 来管理的
return s;
// 所以在返回 Py_None 的时候,我们需要手动将引用计数加 1,因为它作为了返回值。
// 如果你不加 1,那么当你无限调用的时候,总会有那么一刻,Py_None 会被销毁,因为它的引用计数在不断减少
// 但当销毁 Py_None 的时候,会出现 Fatal Python error: deallocating None,解释器异常退出
不过这里还存在一个问题,那就是我们在 C 中返回的是 Python 传过来的。
static PyObject *
f(PyObject *self, PyObject *val)
//传递过来一个 PyObject *,然后原封不动的返回
return val;
显然上面 val 指向的内存不是在 C 中调用 api 创建的,而是 Python 创建然后传递过来的,也就是说这个 val 已经指向了一块合法的内存(和增加 Py_None 引用计数类似)。但是内存中的对象的引用计数是没有变化的,虽说有新的变量(这里的 val)指向它了,但是这个 val 是 C 中的变量不是 Python 中的变量,因此它的引用计数是没有变化的。然后作为返回值返回之后,指向对象的引用计数减一。所以你会发现在 Python 中,创建一个变量,然后传递到 f 中,执行完之后再进行打印就会发生段错误,因为对应的内存已经被回收了。如果能正常打印,说明在 Python 中这个变量的引用计数不为 1,也可能是小整数对象池、或者有多个变量引用,那么就创建一个大整数或者其他的对象多调用几次,因为作为返回值,每次调用引用计数都会减1。
static PyObject *
f(PyObject *self)
// 假设创建一个 PyListObject
PyObject *l1 = PyList_New(2);
// 将 l1 赋值给 l2,但是不好意思,这两位老铁指向的 PyListObject 的引用计数还是 1
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。
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,对应函数格式如下:
static PyObject *
f(PyObject *self)
2. 接受一个参数,METH_O,对应函数格式如下:
static PyObject *
f(PyObject *self, PyObject *val)
3. 接受任意个位置参数,METH_VARARGS,对应函数格式如下:
static PyObject *
f(PyObject *self, PyObject *args)
4. 接受任意个位置参数和关键字参数,METH_VARARGS | METH_KEYWORDS,对应函数格式如下:
static PyObject *
f(PyObject *self, PyObject *args, PyObject *kwargs)
第一种和第二种显然都很简单,关键是第三种和第四种要怎么做呢?我们先来看看第三种,解析多个位置参数可以使用一个函数:PyArg_ParseTuple。
解析多个位置参数
函数原型: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 *
我们举个栗子:
static PyObject *
f(PyObject *self, PyObject *args)
// 目前我们定义了一个 PyObject *args,如果是 METH_O,那么这个 args 就是对应的一个参数
// 如果 METH_VARAGRS,还是只需要定义一个 *args 即可,只不过此时的 *args 是一个 PyTupleObject,我们需要将多个参数解析出来
//假设此时我们这个函数是接收 3 个 int,然后相加
int a, b, c;
下面我们需要使用 PyArg_ParseTuple 进行解析,因为我们接收三个参数
这个函数返回一个整型,如果失败会返回 0,成功返回非 0
if (!PyArg_ParseTuple(args, "iii", &a, &b, &c)){
// 失败我们需要返回 NULL
return NULL;
return PyLong_FromLong(a + b + c);
我们还是编译一下,当然编译的过程我们就不显示了,跟之前是一样的。并且为了方便,我们的模块名就不改了,但是编译之后的 pyd 文件内容已经变了。不过需要注意的是,我们说编译之后会有一个 build 目录,然后会自动把里面的 pyd 文件拷贝到 site-packages 中,如果你修改了代码,但是模块名没有变的话,那么编译之后的文件名还和原来一样。如果一样的话,那么由于已经存在相同文件了,可能就不会再拷贝了。因此两种做法:要么你把模块名给改了,这样编译会生成新的模块。要么编译之前记得把上一次编译生成的 build 目录先删掉,我们推荐第二种做法,不然 site-packages 目录下会出现一大堆我们自己定义的模块。
然后我们将 ml_flags 改成 METH_VARARGS,来测试一下。
#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[] = {
(PyCFunction) f,
// 这里需要改成 METH_VARAGRS,这个地方很重要,因为它表示了函数的参数类型。如果这个地方不修改的话,Python 在调用函数时会发生段错误
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",
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 文件无关。
import kagura_nana
# 传参不符合,自动给你报错
print(kagura_nana.f())
except TypeError as e:
print(e) # function takes exactly 3 arguments (0 given)
print(kagura_nana.f(123))
except TypeError as e:
print(e) # function takes exactly 3 arguments (1 given)
print(kagura_nana.f(123, "xxx", 123, 123))
except TypeError as e:
print(e) # function takes exactly 3 arguments (4 given)
kagura_nana.f(123, 123.0, 123) # int: 123, long: 123, float: 123.000000, double: 123.000000
except TypeError as e:
print(e) # integer argument expected, got float
print(kagura_nana.f(123, 123, 123)) # 369
怎么样,是不是很简单呢?当然 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 文件为例:
def foo(lst: list):
假设我们传递一个列表, 然后返回一个元组, 并且将里面的元素都设置成元素的类型
:return:
return tuple([type(item) for item in lst])
print(foo([1, 2, "3", {}])) # (<class 'int'>, <class 'int'>, <class 'str'>, <class 'dict'>)
如果使用 C 来编写扩展的话,要怎么做呢?
#include "Python.h"
static PyObject *
foo(PyObject *self, PyObject *args)
PyObject *lst; // 首先我们这里要接收一个 PyObject *
// 我们要修改 lst,让它指向我们传递的列表, 因此要传递一个二级指针进行修改
if (!PyArg_ParseTuple(args, "O", &lst)){
return NULL;
// 计算列表中的元素个数,申请同样大小的元组。
// 其实还可以使用 PyList_Size,底层也是调用了 Py_SIZE,只是 PyList_Size 会进行类型检测,同理还有 PyTuple_Size 等等
Py_ssize_t arg_count = Py_SIZE(lst);
// 申请完毕之后,里面的元素全部是 NULL,然后我们来进行设置
// 但是这里我们故意多申请一个,我们看看 NULL 在 Python 中的表现是什么
PyObject *tpl = PyTuple_New(arg_count + 1);
// 申明类型对象、以及元素
PyObject *type, *val;
for (int i = 0; i < arg_count; i++) {
val = PyList_GetItem(lst, i); // 获取对应元素,赋值给 val
// 获取对应的类型对象,但得到的是 PyTypeObject *,所以需要转成 PyObject *
// 或者你使用 Py_TYPE 这个宏也可以,内部自动帮你转了
type = (PyObject *)val -> ob_type;
//设置到元组中
PyTuple_SetItem(tpl, i, type);
return tpl;
static PyMethodDef methods[] = {
"foo",
(PyCFunction) foo,
// 记得这里写上 METH_VARARGS, 假设我们写的是 METH_NOARGS, 那么即便我们上面定义了参数也是没有意义的
// 调用的时候 Python 会提示你: TypeError: foo() takes no arguments
METH_VARARGS,
{NULL, NULL, 0, NULL}
static PyModuleDef module = {
PyModuleDef_HEAD_INIT,
"kagura_nana",
"this is a module named kagura_nana",
methods,
NULL, NULL, NULL, NULL
PyMODINIT_FUNC
PyInit_kagura_nana(void)
return PyModule_Create(&module);
然后使用 Python 测试一下:
import kagura_nana
print(
kagura_nana.foo([1, 2, "3", {}])
) # (<class 'int'>, <class 'int'>, <class 'str'>, <class 'dict'>, <NULL>)
# 我们看到得到结果是一致的,并且我们多申请了一个空间,但是没有设置,所以结尾多了一个 <NULL>
# 但是注意:不要试图通过 kagura_nana.foo([1, 2, "3", {}])[-1] 的方式来获取这个 NULL,会造成段错误
# 因为 Python 操作指针会自动操作指针指向的内存,而 NULL 是一个空指针,指向的内存是非法的
# 另外段错误是一种非常可怕的错误,它造成的结果就是解释器直接就异常退出了。
# 并且这不是异常捕获能解决的问题,异常捕获也是解释器正常运行的前提下。因此申请容器的时候,要保证元数个数相匹配
从这里我们也能看出使用 C 来为 Python 写扩展是一件多么麻烦的事情,因此 Cython 的出现是一个福音。当然我们上面的代码只是演示,没有太大意义,完全可以用 Python 实现。
传递字符串
然后我们再来看看字符串的传递,比较简单,说白了这些都是 Python / C API 的调用。
#include "Python.h"
static PyObject *
f1(PyObject *self, PyObject *args)
// 这里我们接受任意个字符串,然后将它们拼接在一起,最后放在列表中返回。
// 由于是任意个,所以无法使用 PyArg_ParseTuple 了
// 因为我们不知道占位符要写几个 O,但我们说 args 是一个元组,那么我们可以按照元组的方式进行解析
Py_ssize_t arg_count = Py_SIZE(args); // 计算元组的长度
PyObject *res = PyUnicode_FromWideChar(L"", 0); // 返回值,因为包含中文,所以是宽字符
for (int i=0; i < arg_count; i++){
// 将 res 和 里面的字符串依次拼接,等价于字符串的加法
res = PyUnicode_Concat(res, PyTuple_GetItem(args, i));
// 我们上面这种做法比较笨,直接通过 PyUnicode_Join 直接拼接不香吗?我们目前先这么做,join 的话在下面的 f2 函数中
// 然后创建一个列表,将结果放进去。我们申请列表,容量只需要为 1 即可
PyObject *lst = PyList_New(1);
PyList_SetItem(lst, 0, res);
// 我们说 lst 是在 C 中创建的, 但是它作为了返回值, 所以我们不需要关心它的引用计数, 因为会自动减一
// 那 res 怎么办?它要不要减少引用计数,答案是不需要、也不能,因为它作为了容器的一个元素(这里面有很多细节,我们暂且不表,在后面介绍 PyDictObject 的时候再说)
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,
"f2",
(PyCFunction) f2,
METH_VARARGS,
{NULL, NULL, 0, NULL}
static PyModuleDef module = {
PyModuleDef_HEAD_INIT,
"kagura_nana",
"this is a module named kagura_nana",
methods,
NULL, NULL, NULL, NULL
PyMODINIT_FUNC
PyInit_kagura_nana(void)
return PyModule_Create(&module);
Python 进行调用,看看结果。
import kagura_nana
print(kagura_nana.f1("哼哼", "嘿嘿", "哈哈")) # ['哼哼嘿嘿哈哈']
print(kagura_nana.f2("哼哼", "嘿嘿", "哈哈")) # 哼哼||嘿嘿||哈哈
我们看到结果是没有问题的,还是蛮有趣的。
类型检查和返回异常
在 Python 中,当我们传递的类型不对时会报错。那么在底层我如何才能检测传递过来的参数是不是想要的类型呢?首先我们想到的是通过 ob_type,假设我们要求 val 是一个 int,那么:
#include "Python.h"
static PyObject *
f1(PyObject *self, PyObject *val)
// 获取类型名称, 如果是字符串,那么 tp_name 就是 "str",字典是 "dict"
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, 0, NULL}
static PyModuleDef module = {
PyModuleDef_HEAD_INIT,
"kagura_nana",
"this is a module named kagura_nana",
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
写法非常固定,因此我们上面的判断逻辑就可以进行如下修改:
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 即可。
#include "Python.h"
static PyObject *
f1(PyObject *self, PyObject *args)
Py_ssize_t arg_count = Py_SIZE(args);
if (arg_count != 3) {
// 这里是我们设置的异常, 其实参数个数不对的话, 我们可以借助于 PyArg_ParseTuple 来帮助我们
// 因为指定的占位符已经表明了参数的个数
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);
// 这里我们将列表给返回, 而它是 Python 传递过来的, 所以一旦返回、引用计数会减一, 因此我们需要手动加一
Py_INCREF(c);
return c;
static PyMethodDef methods[] = {
"f1",
(PyCFunction) f1,
METH_VARARGS,
{NULL, NULL, 0, NULL}
static PyModuleDef module = {
PyModuleDef_HEAD_INIT,
"kagura_nana",
"this is a module named kagura_nana",
methods,
NULL, NULL, NULL, NULL
PyMODINIT_FUNC
PyInit_kagura_nana(void)
return PyModule_Create(&module);
所以逻辑就是像上面那样,通过 PyErr_Format 来设置异常,这个会被 Python 端接收到,但是异常一旦设置,就必须要返回 NULL,否则会出现段错误。但反过来吗,返回 NULL 的话则不一定要设置异常,但如果你不设置,那么 Python 底层会默认帮你设置一个 SystemError,并且异常的 value 信息为:<built-in function f1> returned NULL without setting an error,提示你返回了 NULL 但没有设置 error。因为返回 NULL 表示程序需要终止了,那么就应该把为什么需要终止的理由告诉使用者。
然后我们来测试一下:
import kagura_nana
kagura_nana.f1()
except Exception as e:
print(e) # >>>>>> f1() takes 3 positional arguments but 0 were given
kagura_nana.f1(1, 2, 3, 4)
except Exception as e:
print(e) # >>>>>> f1() takes 3 positional arguments but 4 were given
kagura_nana.f1(1, 2, 3)
except Exception as e:
print(e) # The 2th argument requires a str, but got int
lst = ["xx", "yy"]
print(kagura_nana.f1(123, "123", lst)) # ['xx', 'yy', 123, '123']
print(lst) # ['xx', 'yy', 123, '123']
所表现的一切,都和我们在底层设置的一样。另外我们再来看看这个函数的身份是什么:
import kagura_nana
def foo(): pass
print(kagura_nana.f1) # <built-in function f1>
print(sum) # <built-in function sum>
print(foo) # <function foo at 0x000001F1BAAF61F0>
我们居然实现了一个内置函数,怎么样是不是很神奇呢?因为扩展模块里面的函数和解释器内置的函数本质上都是一样的,所以它们都是 built-in。
返回布尔类型和 None
我们说函数都必须返回一个 PyObject *,如果这个函数没有返回值,那么在 Python 中实际上返回的是一个 None,但是我们不能返回 NULL,None 和 NULL 是两码事。在扩展函数中,如果返回 NULL 就表示这个函数执行的时候,不符合某个逻辑,我们需要终止掉,不能再执行下去了。这是在底层,但是在 Python 的层面,你需要告诉使用者为什么不能执行了,或者说底层的哪一行代码不满足条件,因此这个时候我们会在 return NULL 之前需要手动设置一个异常,这样在 Python 代码中才知道为什么底层函数退出了。当然有时候会自动帮我们设置,比如们说的 PyArg_ParseTuple。
那么在底层如何返回一个 None 呢?既然要返回我们就需要知道它的结构是什么。
# 首先在 Python 中,None 也是有类型的
print(type(None)) # <class 'NoneType'>
这个 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。
函数原型: int PyArg_ParseTupleAndKeywords(PyObject *args, PyObject *kw, const char *format, char *keywords[], ...)
我们看到相比原来的 PyArg_ParseTuple,多了一个 kw 和一个 char * 类型的数组,具体怎么用我们在编写代码的时候说。
#include "Python.h"
static PyObject *
f1(PyObject *self, PyObject *args, PyObject *kwargs)
// 我们说函数既可以通过位置参数、还可以通过关键字参数传递,那么函数的参数类型就要变成 METH_VARARGS | METH_KEYWORDS
// 参数 args 就是 PyTupleObject 对象, kwargs 就是 PyDictObject 对象
// 假设我们定义了三个参数,name、age、place,这三个参数可以通过位置参数传递、也可以通过关键字参数传递
wchar_t *name;
int age = 17;
wchar_t *gender = L"FEMALE";
// 告诉 Python 解释器参数的名字,注意:里面字符串的顺序就是函数定义的参数顺序
// 这里的字符串就是函数的参数名,上面的是变量名。其实变量名字叫什么无所谓,只是为了一致我们会起相同的名字
char *keys[] = {"name", "age", "gender", NULL};
// 注意结尾要有一个 NULL,否则会报出段错误。
// 解析参数,我们看到 format 中本来应该是 uiu 的,但是中间出现了一个 |
// 这就表示 | 后面的参数是可以不填的,如果不填会使用我们上面给出的默认值
// 因此这里 name 就是必填的,因为它在 | 的前面,而 age 和 gender 可以不填,如果不填就用我们上面给出的默认值
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "u|iu", keys, &name, &age, &gender)){
return NULL;
} // keys 就是函数的所以参数的名字,然后后面把指针传进去,注意顺序要和参数顺序保持一致
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, // 注意这里, 因为支持位置参数和关键字参数, 所以是 METH_VARARGS | METH_KEYWORDS
{NULL, NULL, 0, NULL}
static PyModuleDef module = {
PyModuleDef_HEAD_INIT,
"kagura_nana",
"this is a module named kagura_nana",
methods,
NULL, NULL, NULL, NULL
PyMODINIT_FUNC
PyInit_kagura_nana(void)
return PyModule_Create(&module);
用 Python 来测试一下。
import kagura_nana
print(kagura_nana.f1())
except Exception as e:
print(e) # function missing required argument 'name' (pos 1)
print(kagura_nana.f1(123))
except Exception as e:
print(e) # argument 1 must be str, not int
print(kagura_nana.f1("古明地觉")) # name: 古明地觉, age: 17, gender: FEMALE
print(kagura_nana.f1("古明地恋", 16)) # name: 古明地恋, age: 16, gender: FEMALE
print(kagura_nana.f1("古明地恋", 16, "女")) # name: 古明地恋, age: 16, gender: 女
我们看到一切都符合我们的预期,而且 PyArg_ParseTuple,和 PyArg_ParseTupleAndKeywords 可以自动帮我们检测参数是否合法,不合法抛出合理的异常。当然你也可以检测参数的个数,或者将参数一个一个获取、用 PyXxx_Check 系列检测函数进行判断,看看是否符合预期,当然这么做就比较麻烦了。
PyArg_ParseTuple 和 PyArg_ParseTupleAndKeywords 里面的占位符还可以接收一些特殊的符号,我们举个栗子。为了更好的说明,我们统一以 PyArg_ParseTupleAndKeywords 为例。
占位符 :
下面的是之前写的 C 代码,我们不做任何改动,来测试一下当参数传递错误时的报错信息。
#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, 0, NULL}
static PyModuleDef module = {
PyModuleDef_HEAD_INIT,
"kagura_nana",
"this is a module named kagura_nana",
methods,
NULL, NULL, NULL, NULL
PyMODINIT_FUNC
PyInit_kagura_nana(void)
return PyModule_Create(&module);
我们用 Python 来测试一下,注意观察报错信息。
import kagura_nana
print(kagura_nana.f1())
except Exception as e:
print(e) # function missing required argument 'name' (pos 1)
print(kagura_nana.f1("古明地觉", xxx=123))
except Exception as e:
print(e) # 'xxx' is an invalid keyword argument for this function
print(kagura_nana.f1("古明地觉", name=123))
except Exception as e:
print(e) # argument for function given by name ('name') and position (1)
报错信息似乎没有什么特别的,但是注意了,我们来做一下改动。
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "u|iu:abcdefg", keys, &name, &age, &gender)){
return NULL;
其它地方都不变,我们只在 format 字符串的结尾加上了一个 :abcdefg
,然后编译再来测试一下。
import kagura_nana
print(kagura_nana.f1())
except Exception as e:
print(e) # abcdefg() missing required argument 'name' (pos 1)
print(kagura_nana.f1("古明地觉", xxx=123))
except Exception as e:
print(e) # 'xxx' is an invalid keyword argument for abcdefg()
print(kagura_nana.f1("古明地觉", name=123))
except Exception as e:
print(e) # argument for abcdefg() given by name ('name') and position (1)
你看到了什么?没错,默认的报错信息使用的是 function,但我们通过在占位符中指定 :xxx
,可以将 function 变成我们指定的内容 xxx,一般和函数名保持一致。另外需要注意的是,:xxx
要出现在占位符的结尾,并且只能出现一次。如果这样的话会变成什么样子呢?
PyArg_ParseTupleAndKeywords(args, kwargs, "u:aaa|iu:abcdefg", keys, &name, &age, &gender)
显然这变成了只接受一个参数,然后我们将参数不对时、返回报错信息中的 function 换成了 aaa|iu:abcdefg
。并且你在传递参数的时候还会报出如下错误:
SystemError: More keyword list entries (3) than format specifiers (1)
因为占位符中相当于只有一个 u,也就是接收一个参数,但是我们后面跟了 &name、&age、&gender。关键字 entry 是 3,占位符是 1,两者不匹配。因此 :xxx
一定要出现在最后面,并且只能出现一次。
另外,即使函数不接收参数我们也是可以这么做的,比如:
#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, 0, NULL}
static PyModuleDef module = {
PyModuleDef_HEAD_INIT,
"kagura_nana",
"this is a module named kagura_nana",
methods,
NULL, NULL, NULL, NULL
PyMODINIT_FUNC
PyInit_kagura_nana(void)
return PyModule_Create(&module);
import kagura_nana
print(kagura_nana.f1("xxx"))
except Exception as e:
print(e) # function takes at most 0 arguments (1 given)
然后我们加上 :xxx
。
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
print(kagura_nana.f1("xxx"))
except Exception as e:
print(e) # 123() takes at most 0 arguments (1 given)
我们看到返回信息也被我们修改了,以上就是 :xxx
的作用。所以目前我们看到了两个特殊符号,一个是 |
用来实现默认参数,一个是这里的 :
用来自定义报错信息中的函数名。
占位符 !
我们说占位符 O 表示接收一个 Python 中的对象,但这个对象显然是没有限制的,可以是列表、可以是字典等等。我们之前是通过 Check 的方式进行检测,但是 Python 底层为我们提供更简便的做法,先来看一个常规的例子:
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 *,但如果我希望第一个参数的类型是浮点型,第三个参数的类型是字典,这个时候该怎么做呢?此时 ! 就派上用场了。
static PyObject *
f1(PyObject *self, PyObject *args, PyObject *kwargs)
char *keys[] = {"val1", "val2", "val3", NULL};
PyObject *val1;
PyObject *val2;
PyObject *val3;
// 我们希望限制第一个参数和第三个参数的类型, 那么在它们的后面加上 ! 即可
// 但是注意: 一旦加上了 !, 那么 O! 就要对应两个位置(分别是类型和变量, 当然都是指针)
// 我们说, 第一个参数是浮点型, 那么第一个 O! 对应 &PyFloat_Type, &val1
// 第二个参数没有限制, 那么就是 &val2
// 第三个参数是字典, 那么最后一个 O! 对应 &PyDict_Type, &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;
然后其它地方不变,我们来编译测试一下。
import kagura_nana
print(kagura_nana.f1(123, 123, "xx"))
except Exception as e:
print(e) # my_func() argument 1 must be float, not int
print(kagura_nana.f1(123.0, 11, "xx"))
except Exception as e:
print(e) # my_func() argument 3 must be dict, not str
这个功能就很方便了,可以让我们更加轻松地限制参数类型。但如果你用过 Cython 的话,你会发现我这里所说的方便实在是不敢恭维。如果你要写扩展,那么我强烈推荐 Cython,而且用 Cython 可以轻松的连接 C / C++。
注意:! 只能跟在 O 的后面。
& 的话,对于我们编写扩展而言用的不是很多,首先 & 和 上面说的 ! 用法类似,并且都只能跟在 O 的后面。O! 的话,我们说会对应一个类型指针和一个 PyObject *(参数就会传递给它),会判断传递的参数的类型是否和指定的类型一致。但 O& 的话,则是对应一个函数(convert)和一个任意类型的指针(address),会执行 convert(object, address)
,这个 object 就是我们传递过来的参数。我们举个栗子:
void convert(PyObject *object, long *any){
// 将 object 转成 long, 赋值给 *any
*any = PyLong_AsLong(object);
static PyObject *
f1(PyObject *self, PyObject *args, PyObject *kwargs)
char *keys[] = {"val1", NULL};
long any = 0;
// 我们传递一个 Python 中的整数(假设为 PyObject *val1), 那么这里就会执行 convert(val1, &any)
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&", keys,
convert, &any)){
return NULL;
// 执行完毕之后, any 就会被改变, 为了方便我们就直接打印一下吧, 顺便加一个 1
printf("any = %ld\n", any + 1);
Py_INCREF(Py_None);
return Py_None;
我们来测试一下:
print(kagura_nana.f1(123))
any = 124
效果大概就是这样,个人觉得对于我们编写扩展而言用处不是很大,了解一下即可。
占位符 ;
占位符 ;
和 :
比较类似,但 ;
更加粗暴。至于怎么个粗暴法,看个栗子就一目了然了。
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;
然后我们来调用试试,看看会有什么结果:
import kagura_nana
print(kagura_nana.f1())
except Exception as e:
print(e) # function missing required argument 'val1' (pos 1)
print(kagura_nana.f1(123, 123))
except Exception as e:
print(e) # function takes at most 1 argument (2 given)
目前来看的话,似乎一切正常,但是往下看:
此时把整个报错信息都给修改了,因此这个符号也不是很常用。
注意:;
同样需要放到结尾,并且和 :
相互排斥,两者不可同时出现。
占位符 $
老规矩,还是先来看一个常规的例子。
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))
以上都是没有问题的,可以通过位置参数传递、也可以通过关键字参数传递,只要位置参数在关键字参数之前即可。但如果我们希望某个参数只能通过关键字的方式传递呢?
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;
重新编译然后测试:
import kagura_nana
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))
# 以上仍然是正常的, 都会打印 None
# 但是下面不行了, 因为 val3 必须通过关键字参数的方式传递
kagura_nana.f1(123, 123, 123)
except Exception as e:
print(e) # function takes exactly 2 positional arguments (3 given)
# 其实这就等价于如下:
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 的占位符是一致的,但是功能相反。
我们只接用官方的栗子,因为官方给的栗子非常直观。
Py_BuildValue("") None
Py_BuildValue("i", 123) 123
Py_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))
如果是多个符号,自动会变成一个元组。我们来测试一下:
#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, 0, NULL}
static PyModuleDef module = {
PyModuleDef_HEAD_INIT,
"kagura_nana",
"this is a module named kagura_nana",
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'}]