如何在C/C+中嵌入Python脚本?

本文字数

3500余字

阅读时间

9分钟

01

引言

所周知,PHP是最好的语言Python是当下在人工智能领域常用的编程语言之一,也是近年极为热门的编程语言,作为一种易用的动态语言,我们常常能利用Python快速地编写出一些小型程序来满足需求。而C/C++作为我们大部分人第一门正式学习的编程语言,为我们的程设、数算、AI挑战赛、电设立下了汗马功劳。

Python可以用于快速构建一些小型程序,而C/C++能够更好地处理与系统、硬件相关的底层。那么是否有一种办法让我们结合两者的优势呢?

那就是,使用Python提供的Python/C API将Python脚本嵌入C/C++程序中。

02

准备工作

先,让我们来愉快地配环境吧。既然要将Python脚本嵌入C/C++,我们就需要支持C API的CPython,而不是PyPy或者JPython,(从Python官网下就好了啦),然后再来个C/C++的IDE就好了。

以下我们选用Python3.6.x与宇宙第一IDE Visual Studio 2017来进行介绍。

注意:安装的Python版本位数(x86/x64)要与目标C/C++程序的位数一致!

安装好了Python,来创建一个VS工程吧。

在Python3.6.x中,Python/C API是以一个头文件的形式提供的,这个头文件叫做Python.h,存放在Python安装目录的/include文件夹下,而具体实现存放在安装目录下文件夹/libs中的一个静态链接库python36.lib,那么我们就需要修改VS工程的相关设置使得VS能帮我们找到头文件并顺利完成链接过程。

我们需要修改的设置有VS项目的包含目录与库目录,它们都在解决方案(右键)属性->VC++目录->包含目录/库目录中,我们只需给它们分别添加对应的目录Python安装目录/include与/libs即可。

03

整体流程与要点

下来,我们将从几个方面来玩转Python/C API,带领你快速了解Python/C API的使用方法与Python脚本的嵌入技巧。

Python解释器

万物基于PyObject

Python脚本导入

函数的获取与调用

变量的转换

引用计数管理

1. Python解释器

作为脚本语言,Python脚本(.py文件)总是运行在一个Python解释器上,由解释器来解释代码,完成所有的工作。那么,要在我们的C/C++程序中运行Python脚本,我们需要为其提供运行环境。为此,我们需要在程序的开头调用Py_Initialize()来显式地启动并初始化解释器,同时在程序的末尾调用Py_Finalize()来关闭解释器并释放资源。

2. 万物基于PyObject

在开始介绍API的使用之前呢,需要先给各位打一剂预防针——在之后的旅程里,你只会见到一种变量类型,那就是PyObject*。

在Python的世界里,一切都是对象,包括函数、模块、变量甚至是数字。而这在Python/C API中,就体现为一切量都是一个PyObject*的形式的指针,它将被用于充当绝大多数无关C/C++与Python间类型转换的API的参数与返回值,所以大家要习惯跟PyObject*打交道哟。

3. Python脚本导入

既然要在C/C++中调用Python脚本,那我们就需要能够将Python脚本导入当前的Python解释器中。在Python中,凡是不直接运行的脚本都可以看作是一个模块,我们就是要利用API导入目标模块,就像Python中import其他模块。

这里,我们需要先把Python脚本放到C/C++可执行文件的生成文件夹下,从而使得可以通过相对路径来搜索Python脚本。在C/C++中,调用PyImport_ImportModule(const char *name)这一API,传入模块名(一般是.py文件名,不需要包含后缀.py),如果导入成功,函数会返回一个指向模块对象的不为空的PyObject*指针。

4. 函数的获取与调用

完成模块导入后,我们可以获取模块中的函数对象,并通过调用函数、传递参数、解析返回值来使用Python脚本中的程序,这一部分我们主要关注四个部分:函数对象的获取、参数的构建、函数调用、返回值解析。

这里所使用的Python脚本的内容如下,包含了import、全局变量、函数这些常用要素:

我们的目标是获取该模块中的函数对象foo,这里的函数foo可以看做是模块中的一个属性。那么我们可以调用PyObject_GetAttrString(PyObject *, const char *)来获取函数对象,这个APi中的第一个参数就是指向模块的指针pmod,第二个参数是这里的函数名"foo",同样的,如果成功,也会返回一个指向函数对象的PyObject*指针。

PS:这一API也可以用于获取全局变量。

至此,我们获得了所需的函数对象,下一步是构建参数。在Python/C API中,除了无参数可以用空指针表示外,其他一切参数都要打包为一个元组,否则会出现错误。Python/C API中,为元组操作提供了一系列API,它们都以PyTuple_开头(这也是API命名的规律),例如PyTuple_New(Py_ssize_t size)可创建一个大小为size的元组,PyTuple_Pack(Py_ssize_t size, ...)可以将size个对象(可变参数中提供的)打包为一个元组,PyTuple_SetItem(PyObject *tuple, Py_ssize_t item, PyObject *obj)可以将提供的tuple对象中第item个值设置为obj。

调用函数的过程就比较简单了,函数作为Python中一种可调用对象,使用PyObject_CallObject(

PyObject *callable_object,PyObject *args)即可,其中第一个参数就是函数对象pfunc,第二个参数就是我们前面创建的参数元组,如果调用成功,同上,会返回一个非空的PyObject*指针。

5. 变量的转换

在前述展示的Python脚本中,foo函数会返回time.time()的值,这是一个指示当前时间的浮点数。为了方便C/C++程序,我们需要将返回值(浮点数对象)转化double类型。

Python/C API提供了一系列基本类型(数字、字符串)的在Python对象与C/C++类型间的转化API,一般被命名为Py[Python类型]_[As/From][C/C++类型],其中Python类型中Long为整数、Float为浮点数、Unicode为字符串,As表示从Python类型转化为C/C++类型、From相反。

这里我们使用PyFloat_AsDouble(result)即可完成转换。

显然,如果Python函数的返回值类型比较复杂,例如列表、字典等,转化过程没有这么容易了,需要使用有关的API进行解析,这里就不赘述。

6. 引用计数管理

最关键的部分来了!!

Python采用基于引用计数的垃圾回收机制来进行自动的资源管理,同时一切变量都持有着某一对象的引用,等到这一变量不再被需要时,对应的引用计数会被减1,当引用计数为0时对象将被删除,这一切在直接编写Python代码时都由Python解释器操作。但是一旦我们使用Python/C API在C/C++程序中创建、修改、传递PyObject对象,对象的引用计数并不会自动改变,而要我们自行增减,如果没有正确的操作,就很有可能造成内存泄漏,产生严重问题。

Python/C API提供了4个宏来管理引用计数,分别是Py_INCREF(op)、Py_XINCREF(op)、Py_DECREF(op)、Py_XDECREF(op)。顾名思义,前两个用于增加指针op指向的对象的引用计数,后两个用于减少。另外,带X的宏可以正确处理空指针,不带X的需要调用者保证op不为空指针。

当一个对象的引用计数被减为0时,这个对象所占用的资源就会被自动回收,从而避免了内存泄漏。

那么,应该在什么时候增加引用计数,在什么时候减少引用计数呢?

在Python/C API中,通过函数返回值得到的PyObject*可以分为两种,一种是返回对象的新的引用,这一种情况就需要手动减少引用计数;另一种是返回某个对象的借用(borrowed reference),对这样的变量,无需减少引用计数,但一旦对象被删除,借用也就自动失效了。

对于返回值是引用的API,较为典型的有Py_BuildValue、PyObject_CallObject、PyLong_FromLong、PyObject_GetAttrString等;对于返回值是借用的API,数量较少,常见的是一些Get相关的函数,如PyTuple_GetItem、PyList_GetItem、PyDict_GetItem、PyDict_GetItemString等。当然受限于篇幅,不可能把所有API的返回值类型一一列举,大家有需要的可以到官网的文档中查询:

Python/C API Reference Manual

https://docs.python.org/3/c-api/intro.html

同时一定要注意的是, PyTuple_SetItem与PyList_SetItem这两个API会接管传递给它们的引用,我们不必对于那些参数执行减少引用计数的过程。

如果在C/C++代码中存在PyObject*的复制,就需要根据情况确认代码的行为是新增引用还是借用,新增引用就需要手动增加引用计数,借用则要保证在对象被销毁前完成操作。

PS:因为引用计数管理的缘故,不建议将Python/C API嵌套使用,例如PyObject_CallObject(func,PyTuple_Pack(1,pargs))

C/C++中新增引用、借用与引用计数管理示例

04

常用API介绍

P

ython/C API的起名十分有规律,大致的组成是Py[操作的对象类型]_[行为],其中前一项一般有Object、Dict、Tuple、List、Float、Import等,后一项根据操作对象的不同类型也不一,但一般都能够让人“望文生义”,整体来说学习成本较低。

这里给大家介绍几个上文没有提过的,较常用的API:

Py_BuildValue

PyRun_SimpleString

Py_SIZE

1. Py_BuildValue

在C/C++中创建Python变量的时候,我们常常会苦恼如何快速地创建Python中最常用的几个数据结构,例如字典、列表与元组,Python/C API提供了一个泛用性极强的函数Py_BuildValue。对于

类似于stdio中的printf函数,Py_BuildValue也首先要求一个格式字符串,此后输入对应的C/C++的基本类型值,就可以生成所需的Python变量。

格式字符串中采用一些字母来提示不同类型的变量、用不同的括号来提示不同的数据序列类型,其中常用的转换规则如下:

s:C风格字符串->Python字符串

i/I:int/unsigned int->Python整型(大写多对应unsigned)

b/B:char/unsigned char->Python整型

h/H:short/unsigned short->Python整型

l:long->Python整型(小写L别看错)

k:unsigned long->Python整型

L:long long->Python整型

K:unsigned long long->Python整型

d:double->Python浮点数

f:float->Python浮点数

(items):提示将中间的内容组织为元组

[items]:提示将中间的内容组织为列表

:提示将中间的内容组织为字典,其中连续的两项自动成为一个键值对

其他一些规则:

如果在一个格式字符串中有两项或以上的独立的值,则默认将其组成元组。

如果格式字符串为空,则生成Python中的None。

例:

Py_BuildValue("sid","Hello",1,0.3) ->('Hello',1,0.3)

Py_BuildValue("[iii]",1,2,3)->[1,2,3]

Py_BuildValue("","boy",1,"girl",2)->{'boy':1,'girl':2}

PS:完整规则参见官方文档

Parsing arguments and building values

https://docs.python.org/3/c-api/arg.html

2. PyRun_SimpleString

这个函数所接受的参数非常简单,只需要传入一个C风格的字符串。它所做的工作非常简单,就是在Python解释器中执行字符串的内容(作为代码),完全等同于在Python的交互式界面键入代码。

虽然功能简单,但是这个函数可以在C/C++程序中快速执行任何需要的Python代码,意义重大。

3. Py_SIZE

这个宏接受一个PyObject*指针op,返回值实际上为op->ob_size。对于可求长度的对象,例如列表、元组、字典,它将返回对应的长度,对于不可求长度的对象结果未定。

因此使用此API时一定要注意传入的对象是否可以求长度。

05

小结

要的内容就介绍到这里,相信大家对于Python/C API有了一定的了解,也知道了如何将Python脚本嵌入C/C++程序中,想要深入了解的童鞋们可以前往Python官网参看官方文档进一步学习,说不定你会发现另一个问题的解答。

如何为Python编写C模块?

撰稿人:张钧

审稿人:周嵩林

  • 发表于:
  • 原文链接:https://kuaibao.qq.com/s/20181208G1BFL000?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

同媒体快讯

扫码关注云+社区

领取腾讯云代金券