热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Jython:在Java程序里运行Python代码

Jython:在Java程序里运行Python代码,Go语言社区,Golang程序员人脉社
Jython:在 Java 程序里运行 Python 代码

在这里插入图片描述
笔者:彭大
有任何疑问欢迎关注微信公众号:网易游戏运维平台。(长按识别上图二维码)
微信公众号原文链接:Jython:在 Java 程序里运行 Python 代码

教你如何使用 Jython 在 Java 程序中嵌入 Python 代码。

前言

众所周知,JVM 在大数据基础架构领域可以说是独占鳌头,当我们需要开发大数据处理的相关组件时,首先会想到要使用的语言便是 Java 和 Scala。相比于 Java,Scala 的代码会更加简洁,但也有着高得多的入门门槛,因此为了保证核心组件的稳定和易于维护,我们多数时候都会更倾向于使用 Java 进行开发。

不过,组件中相对稳定的基本功能和框架尚且不谈,对于那些需要快速灵活变化的部分,使用 Java 进行开发则会有些捉襟见肘。例如,我们在为业务方开发一套通用的实时作业时,业务方需要作业在特定的处理环节中支持通过配置自定义的代码来指定算子的行为,并且在配置发生变化时需要可以在不重启实时作业的情况下进行热更新。直接使用 Java 实现这样的功能无疑会有点力不从心,为此我们就需要借助动态语言的力量了。

实际上,在 JVM 平台上使用动态语言的场景并不少见:Groovy 便是为此而生的一门语言。尽管在部分场景下 Groovy 确实是不错的选择,但对于大数据分析来说,Groovy 并不为多数数据开发人员所熟知,相比之下 Python 会是更好的选择。

目前也有不少的大数据框架支持用户提交运行 Python 代码:

  • Hadoop MapReduce 借助 Hadoop Streaming,使用标准输入流和标准输出流进行进程间的数据交换,可以运行包括 Python 在内任意语言写成的可执行文件
  • Apache Spark 提供了 pyspark 编程入口,其使用了 Py4J 来实现 JVM 与 Python 进程间的高效数据传输
  • Apache Flink 则使用了 Jython 来运行用户的 Python 代码。

最终,我们选择了使用 Jython 来实现这样的功能。Jython 类似于 Groovy,能够与宿主 Java 程序在同一个 JVM 进程中运行,相比于 Hadoop Streaming 或是 Py4J 的方案减少了进程间数据传输的损耗,以换来更高的性能。

但在使用 Jython 的时候我们仍然需要注意几点:

  • 部分 PyPI 包可能无法在 Jython 中运行,尤其是那些包含 C 语言扩展的包
  • 和 Groovy 一样,随意地使用 Jython 可能会导致内存泄漏

目前,Jython 已在 2017 年 6 月发布了 2.7.1 版,支持所有 Python 2.7 语法。尽管距离其上一次发布更新已经过去了很长一段时间,但如果你有兴趣看一下它的源代码仓库的话,你会发现它仍在持续迭代中。

Jython 基本使用

本文剩下的内容会集中介绍如何在 Java 程序中使用 Jython。关于其他使用 Jython 的方式,可以参考 Jython 官方给出的 Jython Book,这里我们便不再赘述。

实际上,Jython 的官方文档也给出了在 Java 中嵌入 Jython 的基本示例,极其简单:

import org.python.util.PythonInterpreter; 
import org.python.core.*; 

public class SimpleEmbedded { 
    public static void main(String[] args) throws PyException { 
        PythonInterpreter interp = new PythonInterpreter();

        System.out.println("Hello, brave new world");
        interp.exec("import sys");
        interp.exec("print sys");

        interp.set("a", new PyInteger(42));
        interp.exec("print a");
        interp.exec("x = 2+2");
        PyObject x = interp.get("x");

        System.out.println("x: "+x);
        System.out.println("Goodbye, cruel world");
    }
}

简单,但并不可用。

首先,PythonInterpreter 是个非常重的类,其中包含了 Jython 用于编译 Python 代码所需的所有资源和上下文信息。你不会想要大量创建这样的实例的。

此外,Jython 的实现导致对 PythonInterpreter.eval 方法的重复调用会对相同的 Python 代码不断重复编译运行,导致内存泄漏。

要解决以上问题,我们需要复用 PythonInterpreter 对象,并尽可能不要调用 PythonInterpreter.eval 方法。

复用 PythonInterpreter 对象十分简单:将其实现为单例维护起来即可。你可以以任何形式实现这样的单例模式,简单起见我们这里直接将其设置为一个 private static final 变量:

public class PythonRunner {

    private static final PythonInterpreter intr = new PythonInterpreter();

    public PythonRunner(String code) {
        // ...
    }
    
    public Object run() {
        // ...
    }
}

要想绕过 PythonInterpreter.eval 并不容易,毕竟这是 PythonInterpreter 提供给我们唯一可以运行指定 Python 代码并获取结果的方法。

Groovy 提供了 GroovyShell.parse 方法,可以对给定的 Groovy 代码进行编译,并返回一个 Script 对象。Groovy 这里做的事情实际上是把客户端给定的 Groovy 代码封装在了一个新的 Java 类中(这个类继承了 Script),因此实际上程序可以使用这个 Script 对象的类创建出新的 Script 对象,即可复用这段 Groovy 代码。

我们同样可以在 Jython 这边实现类似的功能 —— 实际上官方的 Jython Book 有提到类似的做法,名为对象工厂模式。按照 Jython Book 中给出的示例,你可以将你需要使用的 Python 代码放到一个 Python 类中,再进行编译,但考虑到我们的场景比较简单,这里我们就简单地将代码放在一个 Python 函数中即可:

import org.python.core.PyFunction;
import org.python.util.PythonInterpreter;

public class PythonRunner {

    private static final PythonInterpreter intr = new PythonInterpreter();
    
    private static final String FUNC_TPL = String.join("n", new String[]{
        "def func():",
        "    %s",
        "",
    });
    
    private final PyFunction func;
    
    public PythonRunner(String code) {
        // 渲染函数内容
        String[] lines = code.split("n");
        for (int i = 1; i < lines.length; i++)
            lines[i] = "    " + lines[i];
        code = String.join("n", lines);
        code = String.format(FUNC_TPL, code);
        
        // 编译并获取 PyFunction 对象
        intr.exec(code);
        func = (PyFunction) intr.get(funcName);
    }

    public Object run() {
        // 使用 PyFunction 对象的 __call__ 方法,调用指定的 Python 代码
        return func.__call__();
    }
}

功能扩展

目前,你已经学到了如何在 Java 程序中使用 Jython 安全地运行 Python 代码,你可以对上述代码进行进一步的扩展来满足你的需求。这里我再简单介绍下我们做的两个比较有用的扩展。

在 Python 代码中使用 Java 对象

在你使用编译后得到的 PyFunction 对象时,你可能会注意到它的 __call__ 方法可以接收任意个类型为 PyObject 的参数。这是不是说,我们得把我们的 Java 对象转换成 PyObject,我们的 Python 代码才能使用这些 Java 对象呢?

答案是否定的,实际上 Jython 已经实现了类似的自动转换功能。如果你提供的是“标准的” Java 对象,那么 Jython 就会把它 “mock” 成对应的 Python 基本类型对象:

  • 所有的 Java 基本数据类型都会被转换为对应的 Python 基本数据类型(例如 shortintbooleanbool
  • 可以像使用普通 Python dict 对象那样使用 java.util.Map 实例
  • 可以像使用普通 Python list 对象那样使用 java.util.List 实例

举个例子,我们的项目需要使用到 FastJSON 的 JSONObject,而这个类实现了 java.util.Map,因此在我们的 Python 代码中,我们只要将它当做一个普通的 Python dict 来使用就好了:

def func(json):
    if not json['test']:
        json['test'] = True
    return True

值得注意的是,Jython 并不会改变你的对象的类型:如果你在你的 Python 代码中使用 instanceof 的话就会发现,实际上传入对象的类型并未改变。除外,如果你对一个 Java bool 值在 Python 代码中使用 is Trueis False 判断时,你都会得到 False 结果。实际上 Jython 仅仅是为你给定的 Java 对象模拟出了对应的 Python 类型的行为(鸭子类型),但实际上它们依然是不同的类型。

引入 PyPI 包

为了进一步减少我们需要写的 Python 代码量,我们也可以把部分公用的 Python 代码维护在统一的包中,然后在自定义的 Python 代码中 import 并使用它。要做到这一点,首先我们要设置好 sys.path

Jython 默认会把当前工作目录放到 sys.path 中(实际上这应该是所有 Python 解释器的标准行为),所以如果我们需要复用某个自制的 Python 库文件,我们只要将它放在当前工作目录下然后 import 就可以了。但如果我们想要使用 PIP 安装的包,我们就需要额外做一些配置了。

实际上,我们只要把本地的 PIP 安装目录路径放到 Jython 的 sys.path 中即可。有很多种方法可以做到这一点,但最安全的做法就是直接询问本地安装好的 Python:

public class PythonRunner {

    private static final PythonInterpreter intr = new PythonInterpreter();
    static {
        intr.exec("import sys");
    
        try {
            // 启动子进程,运行本地安装的 Python,获取 sys.path 配置
            Process p = Runtime.getRuntime().exec(new String[]{
                "python2", "-c", "import json; import sys; print json.dumps(sys.path)"});
            p.waitFor();
            
            // 从中获取到相关的 PIP 安装路径,放入 Jython 的 sys.path
            String stdout = IOUtils.toString(p.getInputStream());
            JSONArray syspathRaw = JSONArray.parseArray(stdout);
            for (int i = 0; i < syspathRaw.size(); i++) {
                String path = syspathRaw.getString(i);
                if (path.contains("site-packages") || path.contains("dist-packages"))
                    inter.exec(String.format("sys.path.insert(0, '%s')", path));
            }
        } catch (Exception ex) {}
    }

    // ...
}

正如我在一开始所说的那样,并不是所有 PyPI 包都能在 Jython 中运行,尤其是那些包含 C 语言代码的包。因此,在你做更多的尝试前,不妨先在 Jython Shell 中 import 一下你想使用的包,验证一下。

结语

这篇博文一方面是对最近我们在做的工作进行一次总结,同时希望这些经验也能够帮助到大家。

不过,我不会认为 Jython 是个 100% 安全的解决方案 —— 实际上,你在使用的过程中有可能会遇到十分诡异的 Bug,而且 Jython 的 API 和文档也还远算不上是“友好”。但不管怎么说,如果你有和我们类似的需求的话,也不妨尝试一下 Jython。


推荐阅读
  • Python 数据可视化实战指南
    本文详细介绍如何使用 Python 进行数据可视化,涵盖从环境搭建到具体实例的全过程。 ... [详细]
  • 从0到1搭建大数据平台
    从0到1搭建大数据平台 ... [详细]
  • 如何将TS文件转换为M3U8直播流:HLS与M3U8格式详解
    在视频传输领域,MP4虽然常见,但在直播场景中直接使用MP4格式存在诸多问题。例如,MP4文件的头部信息(如ftyp、moov)较大,导致初始加载时间较长,影响用户体验。相比之下,HLS(HTTP Live Streaming)协议及其M3U8格式更具优势。HLS通过将视频切分成多个小片段,并生成一个M3U8播放列表文件,实现低延迟和高稳定性。本文详细介绍了如何将TS文件转换为M3U8直播流,包括技术原理和具体操作步骤,帮助读者更好地理解和应用这一技术。 ... [详细]
  • 零拷贝技术是提高I/O性能的重要手段,常用于Java NIO、Netty、Kafka等框架中。本文将详细解析零拷贝技术的原理及其应用。 ... [详细]
  • Spark与HBase结合处理大规模流量数据结构设计
    本文将详细介绍如何利用Spark和HBase进行大规模流量数据的分析与处理,包括数据结构的设计和优化方法。 ... [详细]
  • 本文介绍如何使用 Python 的 DOM 和 SAX 方法解析 XML 文件,并通过示例展示了如何动态创建数据库表和处理大量数据的实时插入。 ... [详细]
  • window下的python安装插件,Go语言社区,Golang程序员人脉社 ... [详细]
  • 在 Ubuntu 中遇到 Samba 服务器故障时,尝试卸载并重新安装 Samba 发现配置文件未重新生成。本文介绍了解决该问题的方法。 ... [详细]
  • Ansible:自动化运维工具详解
    Ansible 是一款新兴的自动化运维工具,基于 Python 开发,集成了多种运维工具(如 Puppet、CFEngine、Chef、Func 和 Fabric)的优点,实现了批量系统配置、程序部署和命令执行等功能。本文将详细介绍 Ansible 的架构、特性和优势。 ... [详细]
  • Python ATM与购物车项目实战:深入解析三层架构设计
    本文详细解析了Python ATM与购物车项目的三层架构设计,重点介绍了MVC(Model-View-Controller)模式的应用。在用户界面层,系统通过图形化界面与用户进行交互,接收并处理用户的输入数据,随后将这些数据传递给控制层进行进一步处理。该层不仅负责展示信息,还承担了用户请求的初步处理任务。 ... [详细]
  • Web开发框架概览:Java与JavaScript技术及框架综述
    Web开发涉及服务器端和客户端的协同工作。在服务器端,Java是一种优秀的编程语言,适用于构建各种功能模块,如通过Servlet实现特定服务。客户端则主要依赖HTML进行内容展示,同时借助JavaScript增强交互性和动态效果。此外,现代Web开发还广泛使用各种框架和库,如Spring Boot、React和Vue.js,以提高开发效率和应用性能。 ... [详细]
  • 如何撰写适应变化的高效代码:策略与实践
    编写高质量且适应变化的代码是每位程序员的追求。优质代码的关键在于其可维护性和可扩展性。本文将从面向对象编程的角度出发,探讨实现这一目标的具体策略与实践方法,帮助开发者提升代码效率和灵活性。 ... [详细]
  • 2021年Java开发实战:当前时间戳转换方法详解与实用网址推荐
    在当前的就业市场中,金九银十过后,金三银四也即将到来。本文将分享一些实用的面试技巧和题目,特别是针对正在寻找新工作机会的Java开发者。作者在准备字节跳动的面试过程中积累了丰富的经验,并成功获得了Offer。文中详细介绍了如何将当前时间戳进行转换的方法,并推荐了一些实用的在线资源,帮助读者更好地应对技术面试。 ... [详细]
  • 触发器的稳态数量分析及其应用价值
    本文对数据库中的SQL触发器进行了稳态数量的详细分析,探讨了其在实际应用中的重要价值。通过研究触发器在不同场景下的表现,揭示了其在数据完整性和业务逻辑自动化方面的关键作用。此外,还介绍了如何在Ubuntu 22.04环境下配置和使用触发器,以及在Tomcat和SQLite等平台上的具体实现方法。 ... [详细]
  • Java能否直接通过HTTP将字节流绕过HEAP写入SD卡? ... [详细]
author-avatar
洛特大人_382
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有