让你的Python程序像C语言一样快

news2024/11/19 13:40:21

让你的Python程序像C语言也一样快

在《Python性能优化指南–让你的Python代码快x3倍的秘诀》中有提到很多加速Python程序的方法,在随后的系列文章中我也为大家专门介绍了用PyPy加速Python 和 用Numba:一行代码将Python程序运行速度提升100倍。但在所有方法中对Python性能提升最大的还要数用C/C++/Rust来实现核心功能模块,真正地让Python运行地和C语言一样快。本文将带大家学习如何用C语言开发Python模块。

在这里插入图片描述

文章目录

    • C扩展模块
    • 用C语言开发Python接口
      • 编写C函数
      • 封装C函数
        • PyObject
        • PyArg_ParseTuple()
        • PyLong_FromLong(bytes_copied)
      • 编写初始化函数
        • `PyMethodDef`
        • `PyModuleDef`
        • `PyMODINIT_FUNC`
    • C扩展模块工作原理
    • 打包C扩展模块
        • 编写setup.py
        • 构建模块
        • 运行效果
    • 性能对比
    • 总结

C扩展模块

Python最强大的特性之一就是可以调用C/C++/Rust等编译型语言开发的函数或库。这让我们可以将一些性能敏感功能用性能更好的编译型语言实现,从而极大提升Python性能。Python中很多库都是这么做的,比如Numpy, Scipy, Pandas, PyPolars等。

另一方面,可能某些功能是以动态库或静态库的形式提供,此时我们需要调用这些由C/C++编译出的库。这样既可以避免重复发明轮子,又能保证功能的稳定高效。

总结起来,如下2类场景可以考虑用C扩展模块:

  • 追求极致性能
  • 调用现有库

用C语言编写Python模块,需要用到Python API,Python API定义了Python解释器调用C代码所需的各种函数、宏和变量。所有这些都定义在Python.h头文件中。学习用C语言写Python模块的核心就是学习Python.h的使用,本文将用一个具体的例子带你学习如何用C语言从0开始开发一个扩展模块。

用C语言开发Python接口

我们接下来会实现一个C扩展模块,这个模块功能很简单,就是简单地封装C语言的fputs()库函数供Python调用。

C的语言的库函数fputs()用于将字符数组写入文件,其接口定义如下:

int fputs(const char *, FILE *)

这个函数有2个参数:

  1. const char * 表示要写入的字符数组
  2. FILE * 表示待写入的文件指针

fputs()会返回一个整型值,如果操作成功,会返回写入文件的字节数;如果写入失败,会返回EOF。关于fputs()更详细的文档请参考这里。

编写C函数

接下来我们调用fputs()是实现一个简单的功能:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int method_fputs(const char * str, const char * filename) {
    int bytes_copied = -1;
    FILE *fp = fopen(filename, "w");
    bytes_copied = fputs(str, fp);
    fclose(fp);
    
    return bytes_copied;
}

上面的代码很好理解,概括起来就做了4件事:

  1. 以写入模式打开一个filename的文件;
  2. 将字符串str 写入 该文件;
  3. 关闭文件;
  4. 返回写入字节数。

这段代码在C语言环境下运行没问题,但是不能直接运行在Python中。Python要想调用需要对上面的C语言代码进行封装改造。

封装C函数

封装C函数需要用到Python.h中的数据结构和方法,改造后的代码如下:

#include <Python.h>

static PyObject *method_fputs(PyObject *self, PyObject *args) {
    char *str, *filename = NULL;

    /* 解析参数 */
    if(!PyArg_ParseTuple(args, "ss", &str, &filename)) {
        return NULL;
    }
    /* 主体逻辑 */
	int bytes_copied = -1;
    FILE *fp = fopen(filename, "w");
    bytes_copied = fputs(str, fp);
    fclose(fp);

    return PyLong_FromLong(bytes_copied);
}

封装改造后的代码看起来比较怪,但仔细一看我们发现主体逻辑没有变化,只是函数传参、返回值的数据类型发生了变化,由此带来了数据类型转换的工作。可以这里我们要用到:

  • PyObject
  • PyArg_ParseTuple()
  • PyLong_FromLong()

下面逐个介绍这3个关键数据结构和方法。

PyObject

PyObject是用于定义Python对象类型的数据结构。所有Python对象都是PyObject的子类。

PyObject的主要作用是告诉Python解释器将对象指针视为对象,因为Python是弱类型语言,Python中所有的数据类型都是对象,它无法直接识别C语言中的类型,因此需要PyObject进行封装。

PyArg_ParseTuple()

PyArg_ParseTuple()的作用是将Python程序传入的参数解析为C语言类型。该函数接收3组参数:

  • args 是传入的参数列表,类型为PyObject
  • "ss" 指明要解析的数据类型,这里**s表示字符串类型,两个s**表示这里有2个字符串类型参数。更多类型定义请参考这里
  • &str and &filename 是C语言局部变量指针,用于接收解析出的参数值

PyArg_ParseTuple()解析出错时会返回0,如果解析失败,则函数直接返回NULL

PyLong_FromLong(bytes_copied)

PyLong_FromLong()将C语言整型值封装成PyLongObject对象,PyLongObject代表Python中的整型对象。因此整个函数的返回值是PyObject *

编写初始化函数

上面我们已经完成了C扩展模块的主体函数,然而,要想让Python能够调用该函数,还有一些额外的工作要做,我们需要定义模块和方法。

static PyMethodDef CextMethods[] = {
    {"fputs", method_fputs, METH_VARARGS, "Python interface for fputs C library function"},
    {NULL, NULL, 0, NULL}
};


static struct PyModuleDef c_ext_module = {
    PyModuleDef_HEAD_INIT,
    "fputs",
    "Python interface for the C extension module",
    -1,
    CextMethods
};

上面的代码定义了Python解释器如何使用该模块和函数的元信息。下面我们逐个看一下PyMethodDefPyModuleDef 这两个结构都有什么用。

PyMethodDef

为了调用模块中定义的方法,我们首先需要将模块告知Python解释器。PyMethodDef就是用来将模块注册给Python解释器的。这个结构有4个成员用来描述模块,每个成员的含义如下:

  • "fputs" 是用户在Python代码中调用该特定函数的函数名称,我们可以任意命名为一个合法的Python函数名称;
  • method_fputs 是要调用的C函数的名称;
  • METH_VARARGS 是一个标志,告诉Python解释器函数将接受两个PyObject*类型的参数:
    • self 表示模块对象
    • args 是一个包含函数实参的元组。这组参数可以用PyArg_ParseTuple()解析出来
  • 最后的字符串是函数的说明文档

这里需要说明一下,一般情况下,C扩展模块中会有多个方法供Python解释器调用,这就是为什么这里需要定义PyMethodDef数组,并且用一个空结构结尾。

PyModuleDef

正如PyMethodDef用于保存C扩展模块中方法的信息一样,PyModuleDef结构体用于保存模块的信息。跟PyMethodDef不同的是,PyModuleDef不是一个结构体数组,而是用于模块定义的单一结构体。该结构体有9个成员,但不是每一个都是必须的,一般我们需要定义如下五个成员:

  • PyModuleDef_HEAD_INITPyModuleDef_Base类型的成员,建议只有一个值。
  • "fputs" 是C扩展模块名称
  • 接下来的字符串是模块的说明文档。你可以传入NULL表示没有文档,也可以传递const char *指向文档字符串,还可以使用PyDoc_STRVAR()为模块定义文档字符串。
  • -1 表示存储程序状态所需的内存大小。当您的模块需要用于多个子解释器时,这个参数会非常有用,它接受2类值:
    • 负值表示此模块不支持子解释器。
    • 非负值表示启用模块的重新初始化功能,并指定了在每个子解释器会话上分配模块的内存需求。
  • FputsMethods 是方法表引用,就是我们先前定义的PyMethodDef结构体数组。

其他4个参数可以参考PyModuleDef的文档。

PyMODINIT_FUNC

上面我们定义好了C扩展模块和方法结构,当我们在Python程序中import该模块时,会触发调用PyInit_fputs(),这里我们需要创建模块的实例:

PyMODINIT_FUNC PyInit_cextension(void) {
    return PyModule_Create(&c_ext_module);
}

PyModule_Create()接收先前定义的方法结构体fputsmodule的地址,并会返回一个新的PyObject *类型的模块对象。

PyMODINIT_FUNC会隐式地做3件事,

  1. 它隐式地将函数的返回类型设置为PyObject *
  2. 它声明了一切特殊关联
  3. 它将函数声明为外部“C”函数,如果您使用的是C++,它会告诉C++编译器不要对符号进行名称篡改。

C扩展模块工作原理

上面我们从零开始实现了一个C扩展模块,这里我们回过头整体看一下C扩展模块是如何工作的。下图展示了C扩展模块的各组成部分以及他们是如何跟Python解释器交互的:

在这里插入图片描述

图1. C扩展模块与Python解释器的交互

当我们在Python中导入C扩展模块时,PyInit_puts()方法第一个被调用。但是,在将引用返回到Python解释器之前,该函数会继续调用PyModule_Create(),这将初始化PyModuleDefPyMethodDef结构体,它们保存有关模块的元信息。在使用模块中的方法之前将这些信息都初始化好并告知Python解释器是非常合理且必要的。

当上面的工作完成后,模块对象的引用最终返回到Python解释器。下图展示了初始化C扩展模块的内部流程:

在这里插入图片描述

图2. C扩展模块内部初始化流程

PyModule_Create()返回的模块对象引用了模块结构体PyModuleDef,而模块结构体又引用了方法表PyMethodDef。当我们调用C扩展模块中定义的方法时,Python解释器会使用模块对象及其携带的所有引用来执行指定方法。

⚠注意:上面的说法只是一种简单的易于理解的说法,Python解释器背后的实际工作比这个要复杂,这里我剔除了所有细节,给大家呈现的是一个简单易懂的核心工作流程。

最后,让我们看看解释器如何处理C扩展模块函数的实际执行:

在这里插入图片描述

图3. C扩展模块函数执行流程

调用method_fputs()后,程序执行以下步骤:

  1. PyArg_ParseTuple()解析从Python解释器传递来的参数
  2. 将这些参数传递给fputs(),这是C扩展模块函数的核心逻辑
  3. PyLong_FromLong()封装从fputs()返回的值

简而言之,就是解析参数 → \rarr 执行核心逻辑 → \rarr 封装并返回结果这个流程。

打包C扩展模块

由于C扩展模块的核心代码是使用C语言写的,所以我们需要编译后才能真正的使用它。Python提供了distutils来构建C扩展模块。

编写setup.py

我们需要创建setup.py 文件来安装模块,这里我们重点关注C扩展模块部分的特性。一个setup.py 文件的基本构成如下:

from distutils.core import setup, Extension

def main():
    setup(name="cextension",
          version="1.0.0",
          description="Python interface for the C extension module",
          author="jarodyv",
          author_email="jarodyv@gmail.com",
          ext_modules=[Extension("cextension", ["c_ext_module.c"])])

if __name__ == "__main__":
    main()

上面的代码展示需要传递给setup()的标准参数。这里我们重点关注最后一个参数ext_modulesext_modules获取Extensions类的对象列表,Extensions类描述了安装脚本中的单个C或C++扩展模块。这里需要向Extensions的构造函数传递2个参数:

  • name 是模块名
  • [filename] 是模块源代码文件路径列表。

构建模块

有了setup.py文件后,我们就可以编译安装模块了。执行下面的代码,会在当前目录下编译并安装C扩展模块:

$ python setup.py install

默认情况下,Python解释器使用clang编译C代码。如果需要使用gcc或其他C编译器,则需要在安装脚本中或直接在命令行中相应地设置CC环境变量。例如,您可以用下面的命令告诉Python解释器使用gcc编译和构建模块:

$ CC=gcc python setup.py install

当然,如果clang不可用,Python解释器也会自动尝试用gcc编译。

运行效果

编译安装成功后,我们就可以在Python中使用该C扩展模块了!下面是在IPython交互式环境下的运行结果:

在这里插入图片描述

图4. C扩展模块调用结果

从上面的输出可以看到,我们的C扩展模块运行正常。

性能对比

最后我们对比一下纯Python和C扩展的性能。上面的例子太多简单,我们重新写一个稍微复杂一点的例子。我们知道在密码学上经常用到大数分解,即将一个很大的数分解成两个质数的乘积,我们就用C扩展实现质因数分解,然后跟纯Python实现进行性能对比。

我们首先来实现C语言版:

int is_prime(unsigned long long n) {
	for(unsigned long long i = 2; i*i<=n; i++) {
		if (n%i==0) {
			return 0;
		}
	}
	return 1;
}

static PyObject *method_prime_factorize(PyObject *self, PyObject *args){
    unsigned long long s;
    if(!PyArg_ParseTuple(args, "K", &s)) {
        return NULL;
    }
    for (unsigned long long i = 2; i*i<=s; i++) {
		if (s % i == 0 && is_prime(i)) {
			unsigned long long j = s / i;
			if(is_prime(j)) {
			    return PyLong_FromLong(i);
			}
		}
	}
	return PyLong_FromLong(1);
}

然后再方法表中加入方法prime_factorize

static PyMethodDef CextMethods[] = {
    {"fputs", method_fputs, METH_VARARGS, "Python interface for fputs C library function"},
    {"prime_factorize", method_prime_factorize, METH_VARARGS, "Calculate the prime factorizer of an integer"},
    {NULL, NULL, 0, NULL}
};

重新执行python setup.py install会重新编译C扩展模块,编译成功后我们就可以测试prime_factorize的性能:

import cextension
%timeit cextension.prime_factorize(7140229933)

输出:

在这里插入图片描述

然后再用纯Python实现相同的功能:

def is_prime(n):
    i = 2
    while i * i <= n:
        if n % i == 0:
            return False
        i += 1
    return True

def prime_factorize(n):
    i = 2
    while i * i <= n:
        if n % i == 0 and is_prime(i):
            j = n / i
            if is_prime(j):
                return i
        i += 1

同样用%timeit测试纯Python实现的执行时间

%timeit prime_factorize(7140229933)

输出:

在这里插入图片描述

从输出对比可以看到用C扩展模块的函数平均只需要358微秒,而纯Python实现需要12.9毫秒,C扩展模块函数比纯Python快36倍。

总结

本文带领大家实现了一个C扩展模块,并向大家详细介绍了C扩展模块的运行机制。大家可以参照文章中的步骤自己实现自己的C扩展模块,亲身体验一下这个开发过程并感受一下最终带来的性能提升。

Python API为用C编程语言编写复杂Python接口提供了大量功能,未来我会进一步介绍如何直接调用C语言库以及如何写出更加健壮的C扩展模块。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/72259.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

VM系列振弦读数模块通讯协议

通讯协议是上位机通过 VMXXX 模块支持的数字接口完成信息交互的数据格式、传输步骤、 通讯速率等的一系列预先约定。上位机必须按照本章描述的通讯协议规则来完成与 VMXXX 的数据交互工作。 寄存器机制 VMXXX 内部维护有若干寄存器&#xff0c;模块在寄存器参数值的控制下…

【LeetCode_字符串_逻辑分析】13. 罗马数字转整数

目录考察点第一次&#xff1a;2022年12月8日15:24:16解题思路代码展示优秀的解题思路1. 总结规律2. 字符串替换题目描述13. 罗马数字转整数 考察点 逻辑分析能力 第一次&#xff1a;2022年12月8日15:24:16 解题思路 分析能力有待提高 代码展示 class Solution {public i…

【HTML5期末作业】用HTML+CSS一个兰州交通大学官网网站

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

百度 Android 直播秒开体验优化

作者 | 任雪龙 导读 网络直播功能作为一项互联网基本能力已经越来越重要&#xff0c;手机中的直播功能也越来越完善&#xff0c;电商直播、新闻直播、娱乐直播等多种直播类型为用户提供了丰富的直播内容。随着直播的普及&#xff0c;为用户提供极速、流畅的直播观看体验也越来越…

外汇天眼:什么是外汇动量交易?新手指南

1. 什么是动量交易&#xff1f; 我们需要了解的第一件事是动量到底是什么。势头是字面意义上的趋势强度。动量交易策略涉及仅在强劲的价格趋势方向开仓&#xff0c;利用持续的价格变动&#xff0c;并在趋势逆转之前退出。 动量交易者通常不会担心趋势在哪里结束和开始&#x…

计算机毕业设计ssm+vue基本微信小程序的校园通知小程序系统 uniapp 小程序

项目介绍 随着互联网技术的发发展,计算机技术广泛应用在人们的生活中,逐渐成为日常工作、生活不可或缺的工具。在高校,各种管理系统层出不穷,为校园通知管理开发必要的系统,能够有效的提升管理效率。一直以来,校园通知一直没有进行系统化的管理,学生无法准确掌握高校通知状态,…

35岁以上的那些测试员何去何从?

人人都说IT行业&#xff1a;35岁就是一道坎&#xff01;跨不过就是一道中年危机&#xff0c;跨过了就成养老保险。那么35岁之后&#xff0c;软件测试从业者都去哪了&#xff1f;能力不行&#xff0c;中年危机很多刚入行的测试的新人&#xff0c;毫无经验&#xff0c;但是远远却…

数图互通高校房产管理——移动端微信小程序

数图互通房产管理系统在这方面做得比较全面&#xff1b; 随着移动智能手机功能的应用日益成熟&#xff0c;学校要求使用移动端微信小程序端进行房产清查、房产数据查询功能。可以利用手机等移动设备来现场查看房屋实际使用情况和清查房屋数据&#xff0c;也可以通过移动终端查…

日记:“实战深度学习”-第1天

机缘 说起来有点偶然&#xff0c;在某书APP中随手详细介绍了学习深度学习相关的历程&#xff0c;断断续续应该发了几百个笔记&#xff0c;持续时间有大概&#xff11;.5年左右&#xff0c;然后某一天清华出版社的赵编辑突然找到我&#xff0c;问我能否分享一下学习的过程&#…

Spring源码深度解析:十二、后处理器 BeanPostProcessor

一、前言 文章目录&#xff1a;Spring源码深度解析&#xff1a;文章目录 二、BeanPostProcessor 所谓的BeanPostProcessor翻译过来就是Bean后处理器。 1. 什么是 BeanPostProcessor BeanPostProcessor是 Spring提供给我们的一个非常重要的扩展接口&#xff0c;并且Spring内…

【C++11多线程】线程同步之线程通信:condition_variable

文章目录1.condition_variable2.notify_one()和notify_all()3.wait()3.1 没有第二个参数&#xff1a;while wait()3.2 有第二个参数&#xff1a;wait() lambda需要注意的是&#xff0c;条件变量condition_variable要和互斥锁mutex搭配起来使用。 1.condition_variable cond…

Windows远程连接Redis(Linux)

Windows远程连接Redis&#xff08;Linux&#xff09; 文章目录Windows远程连接Redis&#xff08;Linux&#xff09;1、写在前面2、配置redis.conf3、启动Redis3.1 开启redis服务3.2 启动客户端3.3 Redis命令3.4 查看Redis密码4、关闭Redis5、Java操作Redis1、写在前面 Windows…

安科瑞ARTU100系列模块化远程控制终端单元 开关量输入输出采集模块

安科瑞 王晶淼/刘芳 1.概述 ARTU系列远程终端单元是高性能配电智能化元件&#xff0c;应用于智能配电、工业自动化等领域。ARTU100系列远程终端单元提供开关量输入、开关量输出、模拟量输入、模拟量输出&#xff0c;能够将采集到的信号通过RS485串口、RJ45以太网接口、2G、Lo…

进制+异或

一.调用API String sInteger.toString(十进制&#xff0c;转为0-35进制)&#xff1b; //R进制的字符串转为10进制数 int aInteger.parseInt(s,R); //把R进制的字符串封装成大数类 BigInteger bnew BigInteger(s,R); 二.求R进制下的数位和 2992这个数十进制和为22&#xff0c;…

【计算机视觉】图像分割中FCN、DeepLab、SegNet、U-Net、Mask R-CNN等算法的讲解(图文解释 超详细)

觉得有帮助请点赞关注收藏~~~ 一、FCN分割算法 全卷积神经网络目标分割算法能够端到端的得到每个像素的目标分类结果&#xff0c;与传统的卷积神经网络只能输入固定大小图像和在网络的末端使用几个全连接层得到固定长度的特征向量不同&#xff0c;全卷积神经网络能够接受任意大…

HBase的数据模型和存储原理

HBase的数据模型 HBase中表的逻辑结构 Name Space&#xff08;命名空间&#xff09; 类似于关系型数据库的 DatabBase 概念&#xff0c;每个命名空间下有多个表。HBase有两个自带的命名空间&#xff0c;分别是 hbase 和 default&#xff0c;hbase 中存放的是 HBase 内置的表&a…

向 Excel 和 PowerPoint 添加可编辑地图的指南

本指南规定: 如何将 shapefile 转换为 svg 文件(如果后者不存在) 如何使用 Excel 重命名 svg 文件中的对象以使对象具有正确的地理名称,以及如何将这些添加为 PowerPoint 中的可编辑地图 这样做的目的是为那些没有或不知道该软件的人提供 QGIS 制图的替代方案。通过提供某个…

如何在 Linux 的 shell 里针对特定用户/组来限制某些命令的使用

0-前言 最近&#xff0c;业务侧有个需求&#xff0c;需要禁止特定用户访问linux特定的命令&#xff0c;如禁止用户A使用rm命令。 我们知道&#xff0c;在linux系统中&#xff0c;一切皆文件。 那么&#xff0c;这个问题也可以泛化为&#xff1a; 如何在linux里限制特定用户…

视频理解论文串讲(上)【论文精读】

文章目录1. DeepVedio【CNN 首次 for 视频理解】2. Two-Stream【双流网络】针对上面不同方向&#xff0c;有不同的代表工作LSTM late fusion 方向【Beyond-short-snippets】Early fusion方向【Convolutional fusion】长视频 【TSN temporal segment 分段的思想&#xff0c;good…

5分钟搞定Linux top命令的基本用法

在linux终端中&#xff0c;输入top, 按下Enter&#xff0c;立即进入top界面 如果你操作正确&#xff0c;应该会看到了下面这样的界面。 此情此景&#xff0c;怎么样是不是看懵逼了 不要慌&#xff0c;坐下来耐心听我逼逼赖赖你就会明白这一堆内容是干嘛的 下面介绍每一项分别是…