【香橙派系列教程】(十)C语言调用Python,联合开发

【十】C语言调用Python


Python3环境搭建:(备注:在香橙派 3.0.6版本的镜像里已经默认自带了python3.10的版本,不需要安装,只需要后续安装下python3 dev即可。后续统一采用Orangepizero2_3.0.6_ubuntu_jammy_desktop_xfce_linux5.16.17的系统镜像)

一、搭建编译环境

通过C 语言调用 Python 代码,需要先安装 libpython3 的 dev 依赖库(不同的 ubuntu 版本下, python 版本可能会有差异, 比如ubuntu 22.04 里是 libpython3.10-dev )。

1.安装依赖包

首先我们可以使用以下命令来查看是否存在python的dev包.

这个命令用于列出已安装的与 Python 3 开发相关的库。具体来说,它使用 dpkg -l 列出所有已安装的软件包,然后使用 grep 过滤出包含 “libpython3” 和 “dev” 的行。

在 Ubuntu 或 Debian 等基于 Debian 的系统上,你可以运行以下命令:

dpkg -l | grep libpython3

image-20240623212043124

正常会有类似如下的输出,出现"libpython3"和 “dev”,如libpython3.10-dev即可:

如果没有的话我们可以使用以下命令进行安装

sudo apt install libpython3.10-dev
#sudo apt install libpython3.6-dev   安装什么版本具体看自己是什么版本的系统

2.安装完成的图示

image-20240623214950566

**特殊情况:**报错了呜呜呜, 如果安装和下面一样显示失败的话,那我们就需要进行换源了,请看附录部分image-20240623213452711

二、C语言调用Python

  1. 头文件Python.h,这是Python API的头文件,用于访问Python对象和函数

  2. Py_Initialize();函数初始化Python解释器

  3. PyRun_SimpleString();函数可以执行一段简单的Python代码,例如打印"funny"。需要传递一个字 符串作为参数,表示要执行的

    Python代码,如print (‘funny’)这么一个Python代码字符串。

函数说明:
int PyRun_SimpleString(const char *command)
这是针对下面 PyRun_SimpleStringFlags() 的简化版接口,将 PyCompilerFlags* 参数设为 NULL;传参就是python执行语句。
  1. Py_Finalize()函数关闭Python解释器,并释放资源。
#include "Python.h"
int main()
{
    Py_Initialize(); // 初始化
    PyRun_SimpleString("print ('happy')");
    Py_Finalize(); //释放资源
}

运行这个程序我们要使用以下命令进行编译(我的python版本是Python 3.10)

怎么看是什么版本呢?

ls /usr/include/python 然后按一下TAB键

gcc demo1.c -o demo1 -I /usr/include/python3.10 -lpython3.10//编译,根据自己dev链接库的版本号
./demo1			//运行

加餐:关于头文件引用

/usr/include :Linux系统编程往往需要引用c头文件,linux下,头文件一般存储到`/usr/include

若头文件在此文件夹内,相对路径直接引入即可

#include "Python.h"

若在其他文件夹,gcc编译需要声明-I指定路径

gcc -I /usr/xxx/include yyy.c

三、C语言调用Python无参函数

在C语言中调用Python代码通常涉及到使用Python的C API。Python提供了一组C API,允许在C中调用Python函数、处理Python对象等。

1.API简单介绍

一般的调用流程
    
1.包含Python.h头文件,以便使用Python API。
    
2.使用void Py_Initialize()初始化Python解释器

3.使用PyObject *PyImport_ImportModule(const char *name)和PyObject *PyObject_GetAttrString(PyObject *o, const char *attr_name)获取sys.path对象,并利用 int PyList_Append(PyObject *list, PyObject *item)将当前路径.添加到sys.path中,以便加载当前的Python模块(Python文件即python模块)。 
 	第二种写法:
    可以改成这两句获取sys.path对象和添加当前路径到sys.path中
    PyRun_SimpleString("import sys");
    PyRun_SimpleString("sys.path.append('./')");

4.使用PyObject *PyImport_ImportModule(const char *name)函数导入Python模块,并检查是否有错误。
    
5.使用PyObject *PyObject_GetAttrString(PyObject *o, const char *attr_name)函数获取 Python函数对象,并检查是否可调用。
    
6.使用PyObject *PyObject_CallObject(PyObject *callable, PyObject *args)函数调用 Python函数,并获取返回值。
    
7.使用void Py_DECREF(PyObject *o)函数释放所有引用的Python对象。
    
8.结束时调用void Py_Finalize()函数关闭Python解释器。

相关的函数参数说明参考网站(网站左上角输入函数名即可开始搜索):
https://docs.python.org/zh-cn/3/c-api/import.html

2.示例代码

下面是一个简单的例子,展示了如何在C中调用Python代码:

1.Python模块: 编写一个简单的Python模块,保存为 nopara.py

#nopara.py文件
def say_funny():
    print('funny')

2.C代码: 编写一个C程序 invoke.c,调用Python中的函数。

里面用到了sys.path,具体可以参照附录部分

#include <Python.h>
 
int main(){
	Py_Initialize();//初始化python解释器,他会加载我们python的编译环境
	PyObject *pyob = PyImport_ImportModule("sys");//获取sys模块
	PyObject *list = PyObject_GetAttrString(pyob,"path");//获取sys.path对象
	PyList_Append(list,PyUnicode_FromString("."));//将当前路径添加到sys.path中
    //PyUnicode_FromString函数将C语言字符转换为Python字符
/********************************************************************
	第二种写法:将567行全部替换掉,更牛逼更简单,同样可以运行
	PyRun_SimpleString("import sys");
    PyRun_SimpleString("sys.path.append('./')");
*******************************************************************/
    // 4.导入nopara模块
	PyObject *mode = PyImport_ImportModule("nopara");//导入python模块,就是导入要执行的python文件
	if(!mode){
		PyErr_Print();
		printf("ERROR:mode not!\n");
	}
    
    // 5.获取say_funny函数对象
	PyObject *pFunc = PyObject_GetAttrString(mode,"say_funny");
 
	if(!pFunc|| !PyCallable_Check(pFunc)){
		PyErr_Print();
		printf("ERROR:pFunc not!\n");
    }
    // 6.调用say_funny函数并获取返回值
	PyObject *pValue = PyObject_CallObject(pFunc,NULL);
	if(!pValue){
		PyErr_Print();
		printf("ERROR:pValue not!\n");
	}
    // 7.释放所有引用的Python对象
	Py_DECREF(pValue);
	Py_DECREF(pFunc);
	Py_DECREF(mode);
    // 8.关闭Python解释器
	Py_Finalize();
	return 0;
}

编译语句:

gcc invoke.c -I /usr/include/python3.10 -l python3.10
//“-I”指定python头文件路径;“-l”指定python库文件路径

image-20240624114607130

四、C语言调用Python有参函数

首先定义一个带参数和返回值的函数

def say_funny(category):
    print(category)
    return category

接下来用C语言进行调用,调用的流程无参函数的调用方式几乎是一样的是的:

#if 0
1、包含Python.h头文件,以便使用Python API。
2、使用void Py_Initialize()初始化Python解释器,
3、使用PyObject *PyImport_ImportModule(const char *name)和PyObject *PyObject_GetAttrString(PyObject *o, const
char *attr_name)获取sys.path对象,并利用int PyList_Append(PyObject *list, PyObject *item)将当前路径.添加到sys.path
中,以便加载当前的Python模块(Python文件即python模块)。
4、使用PyObject *PyImport_ImportModule(const char *name)函数导入Python模块,并检查是否有错误。
5、使用PyObject *PyObject_GetAttrString(PyObject *o, const char *attr_name)函数获取Python函数对象,并检查是否可调用。

6、使用PyObject *Py_BuildValue(const char *format, …)函数将C类型的数据结构转换成Python对象,作为Python函数的参数,没有参数不需要调用

7、使用PyObject *PyObject_CallObject(PyObject *callable, PyObject *args)函数调用Python函数,并获取返回值。

8、使用int PyArg_Parse(PyObject *args, const char *format, …)函数将返回值转换为C类型,并检查是否有错误,没有返回值时不需要调用。

9、使用void Py_DECREF(PyObject *o)函数释放所有引用的Python对象。
10、结束时调用void Py_Finalize()函数关闭Python解释器。

#endif

无非多了两步,第六步传参

6、使用PyObject *Py_BuildValue(const char *format, ...)函数创建一个Python元组或者对象,作为Python函数的参数,没有参数势不需要调用

这里要注意的是,Py_BuildValue的第一个参数是类型转换:C对应的Python的数据类型转换对应的格式如下:

关于传参的详细说明请看附录部分

image-20240624160459838

和第八步返回值的处理:

8、使用int PyArg_Parse(PyObject *args, const char *format, ...)函数将返回值转换为C类型,并检查是否有错误,没有返回值时不需要调用。
args: 一个包含传递给函数的参数的元组。
format: 包含格式化指令的字符串,指定了如何解析参数。
...: 用于接收解析后的参数值的变量。

para.py文件

def say_funny(category):
	print(category)
    return category

invoke2.c文件

#include <Python.h>
int main()
{
    Py_Initialize();
    // 将当前路径添加到sys.path中
    PyObject *sys = PyImport_ImportModule("sys");
    PyObject *path = PyObject_GetAttrString(sys, "path");
    PyList_Append(path, PyUnicode_FromString("."));
    // 导入para模块
    PyObject *pModule = PyImport_ImportModule("para");
    if (!pModule)
    {
        PyErr_Print();
        printf("Error: failed to load nopara.py\n");
    }
    //获取say_funny函数对象
    PyObject *pFunc = PyObject_GetAttrString(pModule, "say_funny");
    if (!pFunc)
    {
        PyErr_Print();
        printf("Error: failed to load say_funny\n");
    }
    //创建一个字符串作为参数
    char *category = "comedy";
    PyObject *pArgs = Py_BuildValue("(s)", category);//字符串加括号表示他是一个包含字符串的元组
    
    //调用say_funny函数并获取返回值
    PyObject *pValue = PyObject_CallObject(pFunc, pArgs);
    if (!pValue)
    {
        PyErr_Print();
        printf("Error: function call failed\n");
    }
    //将返回值转换为C类型
    char *result = NULL;
    if (!PyArg_Parse(pValue, "s", &result))
    {
        PyErr_Print();
        printf("Error: parse failed\n");
    }
    //打印返回值
    printf("pValue=%s\n", result);
    //释放所有引用的Python对象
    Py_DECREF(pValue);
    Py_DECREF(pFunc);
    Py_DECREF(pModule);
    //释放所有引用的Python对象
    Py_Finalize();
    return 0;
}

编译:gcc invoke2.c -I /usr/include/python3.10 -l python3.10

运行:./a.out

image-20240624171353294


附录:

1.安装依赖包失败换源

OrangePi ZERO 2 的 apt 软件源由/etc/apt/sources.list设定,换源的话只需要更改里面的内容就可以了。不过为了保险起见,先对配置文件在同目录进行备份,在命令终端输入下面的命令:

sudo cp /etc/apt/sources.list /etc/apt/sources.list.save

如果我们后期发现因为这方面的原因出现了问题,某些源文件使用不了我们就可以直接输入下面的命令

sudo cp /etc/apt/sources.list.save /etc/apt/sources.list

以下是我们需要更换的开源软件镜像源:

deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ jammy main restricted universe multiverse
#deb-src http://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ jammy main restricted universe multiverse
deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ jammy-security main restricted universe multiverse
#deb-src http://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/jammy-security main restricted universe multiverse
deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ jammy-updates main restricted universe multiverse
#deb-src http://mirrors.tuna.tsinghua.edu.cn/ubuntu-portsJammy-updates main restricted unlverse multiverse
deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ jammy-backports main restricted universe multiverse
#deb-src http://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/jammy-backports main restricted unicsbpe@@hv$dr/ghesg

使用以下命令进去把原本的内容都清除掉,打开后,输入ggdG全部删除(先按两下g键,再按一下d键,然后按组合键Shift + g),粘贴上软件镜像源的内容,然后保存退出即可。

修改完毕之后,使用 apt 进行软件包升级了。

sudo vi /etc/apt/sources.list

更新系统

更新系统可以输入以下命令:

sudo apt update

更新完之后直接安装libpython3的 dev依赖库即可

2.sys.path用法介绍

介绍

sys.path指定模块搜索路径的列表。默认情况下,python导入文件或者模块,会在sys.path里找模块的路径。如果在当前搜索路径列表sys.path中找不到该模块的话,就会报错。

用法

最常用的用法就是向sys.path中添加搜索路径

import sys
sys.path.append(path)  # path 代表的是一个路径

举例

1.查看当前搜索路径

如果我要导入python模块,那么会在这些路径下寻找

示例代码:b.py

import sys
print(sys.path)

运行结果:python3 b.py

image-20240624113825932

2.添加新的路径

我现在有一个模块new_package,路径为/home/nsy/nlp/new_package,我直接import new_package是会报错的,因为在上面那些路径中找不到

import sys
import new_package

#报错
>>Traceback (most recent call last):
  File "sys_test.py", line 2, in <module>
    import new_package
ModuleNotFoundError: No module named 'new_package'

添加模块所在路径之后,就可以成功运行啦!!

import sys
sys.path.append('/home/nsy/nlp')
import new_package
print(sys.path)

需要注意的是:添加完之后再看path路径,会增加了自己所需要的路径,即使程序跑完也永远的保存了下来,除非你自己删除掉image-20240624120309506

3.使用 GCC编译并链接 Python 3.10 的共享库

这段命令和代码的作用是:

ls /usr/include/python

  1. 列出 /usr/include/python 目录下的内容。通常,Python 的头文件(.h 文件)会在这个目录下。

gcc invoke.c -o invoke -I /usr/include/python3.10 -lpython3.10

  1. 使用 GCC 编译 invoke.c 文件,并链接 Python 3.10 的共享库。
  2. 其中,-I 选项指定了头文件所在的目录,-lpython3.10 用于链接 Python 3.10 的共享库。
  3. -o编译后的可执行文件名为 invoke。(如果不指定默认生成a.out可执行文件)
  4. ./invoke:运行编译生成的 invoke 可执行文件。

这段命令和代码的目的可能是在 invoke.c 文件中调用了 Python 3.10 的一些函数,并通过 GCC 编译器将其与 Python 3.10 的共享库链接起来。在运行 invoke 可执行文件时,它可能会执行 invoke.c 中的 Python 相关代码。

请确保你的系统中已经安装了 Python 3.10 的开发包,以及 GCC 编译器。如果没有安装,你可以使用系统的包管理工具进行安装。

4.Py_BuildValue函数传参

Py_BuildValue 函数用于将C中的数据转换为Python对象。它的第一个参数是一个格式字符串,指定了如何构建Python对象。以下是一些常见的格式码及其对应的C数据类型:

PythonC类型
‘s’const char *
‘i’int
‘l’long
‘f’double(浮点数)
‘d’double(双精度浮点数)
‘o’PyObject*

例如:如果你想构建一个Python元组,其中包含一个整数和一个字符串,可以使用以下格式字符串:

PyObject* result = Py_BuildValue("(is)", 42, "hello");

这将构建一个包含一个整数和一个字符串的元组。第一个参数 42 对应 i 表示整数,第二个参数 "hello" 对应 s 表示字符串。

以下是一些常用的格式码:

  • '()':元组
  • '[]':列表
  • '{}':字典
  • 's':字符串
  • 'i':整数
  • 'l':长整数
  • 'f':浮点数
  • 'd':双精度浮点数
  • 'O':任意Python对象

你可以根据实际需要选择合适的格式码,并使用相应的C数据类型提供参数。
确保在使用 Py_BuildValue 时,提供的参数数量和格式码匹配,以避免运行时错误。

格式码C数据类型示例
‘s’const char *Py_BuildValue(“s”, “hello”)
‘i’intPy_BuildValue(“i”, 42)
‘l’longPy_BuildValue(“l”, 123456789L)
‘f’floatPy_BuildValue(“f”, 3.14f)
‘d’doublePy_BuildValue(“d”, 2.71828)
‘o’PyObject*Py_BuildValue(“O”, somePyObject)
‘()’TuplePy_BuildValue(“(is)”, 42, “hello”)
‘[]’ListPy_BuildValue(“[i]”, 1)
‘{}’DictionaryPy_BuildValue(“{s:i}”, “key”, 42)

5.PyArg_Parse函数返回值

int PyArg_Parse(PyObject *args, const char *format, …)函数将返回值转换为C类型, 并检查是否有错误,没有返回值时不需要调用

PyArg_Parse 是 Python/C API 中用于将 Python 对象参数转换为 C 数据类型的函数。它通常用于解析从 Python 调用的函数传递的参数。

1.以下是 PyArg_Parse 的基本用法:

int PyArg_Parse(PyObject *args, const char *format, ...);
//参数
1.args: 一个包含传递给函数的参数的元组。
2.format: 包含格式化指令的字符串,指定了如何解析参数。
3.... : 用于接收解析后的参数值的变量。

2.PyArg_Parse 的格式化指令可以是以下之一:

  • 'i': 整数
  • 'f': 浮点数
  • 's': 字符串
  • 'O': 任意对象

3.以下是一些示例:

3.1解析一个整数参数

int value;
if (!PyArg_Parse(args, "i", &value)) {
    // 处理解析错误
    return NULL;
}

3.2 解析一个浮点数参数:

double value;
if (!PyArg_Parse(args, "f", &value)) {
    // 处理解析错误
    return NULL;
}

3.3 解析一个字符串参数:

const char *text;
if (!PyArg_Parse(args, "s", &text)) {
    // 处理解析错误
    return NULL;
}

3.4 解析一个任意对象参数:

PyObject *obj;
if (!PyArg_Parse(args, "O", &obj)) {
    // 处理解析错误
    return NULL;
}

4.PyArg_Parse 还支持更复杂的格式化字符串,允许你指定多个参数以及它们的类型。例如:

int x, y;
if (!PyArg_Parse(args, "(ii)", &x, &y)) {
    // 处理解析错误
    return NULL;
}

在这个例子中,期望参数是一个包含两个整数的元组。

注意:PyArg_Parse 在解析参数时会自动增加引用计数,但在解析失败时需要手动处理错误。如果解析失败,它会返回 0,并且你应该在错误处理中返回 NULL 或适当的错误信息。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

橘猫.exe

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值