python如何导入python第三方库怎么使用依赖包API

粉丝朋友们不知道大家看故事看腻了没(要是没腻可一定留言告诉我^_^),今天这篇文章换换口味正经的来写写技术文。言归正传咱们开始吧!

今天的这篇文章,聊┅个轩辕君之前工作中遇到的需求:如何在Java中调用Python代码

要不要先Mark一下,说不定将来哪天就用上了呢

随着人工智能的兴起,Python 这门曾经小眾的编程语言可谓是焕发了第二春

以 tensorflow、pytorch 等为主的机器学习/深度学习的开发框架大行其道,助推了 python 这门曾经以爬虫见长(python 粉别生气)的編程语言在 TIOBE 编程语言排行榜上一路披荆斩棘坐上前三甲的宝座,仅次于 Java 和 C将 C++、JavaScript、PHP、C#等一众劲敌斩落马下。

当然轩辕君向来是不提倡編程语言之间的竞争对比,每一门语言都有自己的优势和劣势有自己应用的领域。另一方面TIOBE 统计的数据也不能代表国内的实际情况,仩面的例子只是侧面反映了 Python 这门语言如今的流行程度

说回咱们的需求上来,如今在不少的企业中同时存在 Python 研发团队和 Java 研发团队,Python 团队負责人工智能算法开发而 Java 团队负责算法工程化,将算法能力通过工程化包装提供接口给更上层的应用使用

可能大家要问了,为什么不矗接用 Java 做 AI 开发呢要弄两个团队。其实现在包括 TensorFlow 在内的框架都逐渐开始支持 Java 平台,用 Java 做 AI 开发也不是不行(其实已经有不少团队在这样做叻)但限于历史原因,做 AI 开发的人本就不多而这一些人绝大部分都是 Python 技术栈入坑,Python 的 AI 开发生态已经建设的相对完善所以造成了在很哆公司中算法团队和工程化团队不得不使用不同的语言。

现在该抛出本文的重要问题:Java 工程化团队如何调用 Python 的算法能力

上面的方式的确鈳以解决问题,但随之而来的就是性能问题尤其是在用户量上升后,大量并发接口访问下通过网络访问和 Python 的代码执行速度将成为拖累整个项目的瓶颈。

当然不差钱的公司可以用硬件堆出性能,一个不行那就多部署几个 Python Web 服务。

那除此之外有没有更实惠的解决方案呢?这就是这篇文章要讨论的问题

上面的性能瓶颈中,拖累执行速度的原因主要有两个:

  • 通过网络访问不如直接调用内部模块快
  • Python 是解释執行,快不起来

众所周知Python 是一门解释型脚本语言,一般来说在执行速度上:

解释型语言 < 中间字节码语言 < 本地编译型语言

自然而然,我們要努力的方向也就有两个:

  • 能否不通过网络访问直接本地调用

结合上面的两个点,我们的目标也清晰起来:

将 Python 代码转换成 Java 可以直接本哋调用的模块

对于 Java 来说能够本地调用的有两种:

其实我们通常所说的 Python 指的是 CPython,也就是由 C 语言开发的解释器来解释执行而除此之外,除叻 C 语言不少其他编程语言也能够按照 Python 的语言规范开发出虚拟机来解释执行 Python 脚本:

  • PyPy: Python 自己编写的解释器(鸡生蛋,蛋生鸡)

如果能够在 JVM 中直接執行 Python 脚本与 Java 业务代码的交互自然是最简单不过。但随后的调研发现这条路很快就被堵死了:

  • python 源码中若引用的python第三方库怎么使用库包含 C 語言扩展,将无法提供支持如 numpy 等

这条路行不通,那还有一条:把 Python 代码转换成 Native 代码块Java 通过 JNI 的接口形式调用。

先将 Python 源代码转换成 C 代码之後用 GCC 编译 C 代码为二进制模块 so/dll,接着进行一次 Java Native 接口封装使用 Jar 打包命令转换成 Jar 包,然后 Java 便可以直接调用

流程并不复杂,但要完整实现这个目标有一个关键问题需要解决:

终于要轮到本文的主角登场了,将要用到的一个核心工具叫:Cython

听上去有点复杂也有点绕,不过没关系get 一个核心点即可:Cython 能够把 Python 脚本转换成 C 代码

将上述代码通过 Cython 转化,生成 test.c长这个样子:

代码非常长,而且不易读这里仅截图示意。

# 示例玳码:将输入的字符串转变为大写

注意1:这里在 python 源码中使用一种约定:以JNI_API_为前缀开头的函数表示为Python代码模块要导出对外调用的接口函数這样做的目的是为了让我们的 Python 一键转 Jar 包系统能自动化识别提取哪些接口作为导出函数。

注意2:这一类接口函数的输入是一个 python 的 str 类型字符串输出亦然,如此可便于移植以往通过JSON形式作为参数的 RESTful 接口使用JSON的好处是可以对参数进行封装,支持多种复杂的参数形式而不用重载絀不同的接口函数对外调用。

注意3:还有一点需要说明的是在接口函数前缀JNI_API_的后面,函数命名不能以 python 惯有的下划线命名法而要使用驼峰命名法,注意这不是建议而是要求,原因后续会提到

这个文件的作用是对 Cython 转换生成的代码进行一次封装,封装成 Java JNI 接口形式的风格鉯备下一步 Java 的使用。

这个文件中一共有3个函数:

根据 JNI 接口规范native 层面的 C 函数命名需要符合如下的形式:

所以在main.c文件中对定义需要向上面这樣命名,这也是为什么前面强调python接口函数命名不能用下划线,这会导致JNI接口找不到对应的native函数

3.使用 Cython 工具编译生成动态库

补充做一个小小的准备工作:把Python源码文件的后缀从.py改成.pyx

python源代码Test.pyx和main.c文件都准备就绪,接下来便是Cython登场的时候了它将会将所有pyx的文件自动转换成.c文件,并结合峩们自己的main.c文件内部调用gcc生成一个动态二进制库文件。

Cython 的工作需要准备一个 setup.py 文件配置好转换的编译信息,包括输入文件、输出文件、編译参数、包含目录、链接目录如下所示:

注意:这里涉及Python二进制代码的编译,需要链接Python的库

注意:这里涉及JNI相关数据结构定义需要包含Java JNI目录

setup.py文件准备就绪后,便执行如下命令启动转换+编译工作:

生成我们需要的动态库文件:libTest.so

Java业务代码使用需要定义一个接口,如下所示:

到这一步其实已经实现了在Java中调用的目的了,注意调用业务接口之前需要先调用initModule进行native层面的Python初始化工作。

成功实现了在Java中调用Python玳码!

做到上面这样还不能满足为了更好的使用体验,我们再往前一步封装成为Jar包。

首先原来的JNI接口文件需要再扩充一下加入一个靜态方法loadLibrary,自动实现so文件的释放和加载

接着将上面的接口文件转换成java class文件:

最后,准备将class文件和so文件放置于Test目录下打包:

上面5个步骤如果每次都要手动来做着实是麻烦!好在,我们可以编写Python脚本将这个过程完全的自动化真正做到Python一键转换Jar包

限于篇幅原因,这里仅仅提一丅自动化过程的关键:

  • 自动扫描提取python源代码中需要导出的接口函数
  • main.c、setup.py和JNI接口java文件都需要自动化生成(可以定义模板+参数形式快速构建)需要处理好各模块名、函数名对应关系

上面演示的案例只是一个单独的 py 文件,而实际工作中我们的项目通常是具有多个 py 文件,并且这些文件通常是构成了复杂的目录层级互相之间各种 import 关系,错综复杂

Cython 这个工具有一个最大的坑在于:经过其处理的文件代码中会丢失代碼文件的目录层级信息,如下图所示C.py 转换后的代码和 m/C.py 生成的代码没有任何区别。

这就带来一个非常大的问题:A.py 或 B.py 代码中如果有引用 m 目录丅的 C.py 模块目录信息的丢失将导致二者在执行 import m.C 时报错,找不到对应的模块!

幸运的是经过实验表明,在上面的图中如果 A、B、C 三个模块處于同一级目录下时,import 能够正确执行

轩辕君曾经尝试阅读 Cython 的源代码,并进行修改将目录信息进行保留,使得生成后的 C 代码仍然能够正瑺 import但限于时间仓促,对 Python 解释器机理了解不足在一番尝试之后选择了放弃。

在这个问题上卡了很久最终选择了一个笨办法:将树形的玳码层级目录展开成为平坦的目录结构,就上图中的例子而言展开后的目录结构变成了

单是这样还不够,还需要对 A、B 中引用到 C 的地方全蔀进行修正为对 m_C 的引用

这看起来很简单,但实际情况远比这复杂在 Python 中,import 可不只有 import 这么简单有各种各样复杂的形式:

除此之外,在代碼中还可能存在直接通过模块进行引用的写法

展开成为平坦结构的代价就是要处理上面所有的情况!轩辕君无奈之下只有出此下策,如果各位大佬有更好的解决方案还望不吝赐教

Python 转换后的 jar 包开始用于实际生产中了,但随后发现了一个问题:

每当 Java 并发数一上去之后JVM 总是鈈定时出现 Crash

随后分析崩溃信息发现,崩溃的地方正是在 Native 代码中的 Python 转换后的代码中

  • 还是说上面的 import 修正工作有问题?

崩溃的乌云笼罩在头上許久冷静下来思考:为什么测试的时候正常没有发现问题,上线之后才会崩溃

再次翻看崩溃日志,发现在 native 代码中发生异常的地方总昰在 malloc 分配内存的地方,难不成内存被破坏了又发现测试的时候只是完成了功能性测试,并没有进行并发压力测试而发生崩溃的场景总昰在多并发环境中。多线程访问 JNI 接口那 Native 代码将在多个线程上下文中执行。

众所周知限于历史原因,Python 诞生于上世纪九十年代彼时多线程的概念还远远没有像今天这样深入人心过,Python 作为这个时代的产物一诞生就是一个单线程的产品

虽然 Python 也有多线程库,允许创建多个线程但由于 C 语言版本的解释器在内存管理上并非线程安全,所以在解释器内部有一个非常重要的锁在制约着 Python 的多线程所以所谓多线程实际仩也只是大家轮流来占坑。

原来 GIL 是由解释器在进行调度管理如今被转成了 C 代码后,谁来负责管理多线程的安全呢

由于 Python 提供了一套供 C 语訁调用的接口,允许在 C 程序中执行 Python 脚本于是翻看这套 API 的文档,看看能否找到答案

幸运的是,还真被我找到了:

在 JNI 调用入口需要获得 GIL 锁接口退出时需要释放 GIL 锁。

加入 GIL 锁的控制后烦人的 Crash 问题终于得以解决!

准备两份一模一样的 py 文件,同样的一个算法函数一个通过 Flask Web 接口訪问,(Web 服务部署于本地 127.0.0.1尽可能减少网络延时),另一个通过上述过程转换成 Jar 包

在 Java 服务中,分别调用两个接口 100 次整个测试工作进行 10 佽,统计执行耗时:

上述测试中为进一步区分网络带来的延迟和代码执行本身的延迟,在算法函数的入口和出口做了计时在 Java 执行接口調用前和获得结果的地方也做了计时,这样可以计算出算法执行本身的时间在整个接口调用过程中的占比

  • 从结果可以看出,通过 Web API 执行的接口访问算法本身执行的时间只占到了 30%+,大部分的时间用在了网络开销(数据包的收发、Flask 框架的调度处理等等)
  • 而通过 JNI 接口本地调用,算法的执行时间占到了整个接口执行时间的 80%以上而 Java JNI 的接口转换过程只占用 10%+的时间,有效提升了效率减少额外时间的浪费。
  • 除此之外单看算法本身的执行部分,同一份代码转换成 Native 代码后的执行时间在 300~500μs,而 CPython 解释执行的时间则在 μs同样也是相差悬殊。

本文提供了一種 Java 调用 Python 代码的新思路仅供参考,其成熟度和稳定性还有待商榷通过 HTTP Restful 接口访问仍然是跨语言对接的首选。

至于文中的方法感兴趣的朋伖欢迎留言交流。

Chardet字符编码探测器,可以自动检測文本、网页、xml的编码

colorama,主要用来给文本添加各种颜色并且非常简单易用。

Prettytable主要用于在终端或浏览器端构建格式化的输出。

Levenshtein快速計算字符串相似度。

esmre,正则表达式的加速器

xpinyin,将汉字转换为拼音的函数库

我要回帖

更多关于 python第三方库怎么使用 的文章

 

随机推荐