Ejecutar código Python desde Java

1. Introducción

Python es cada vez más popular como lenguaje de programación, especialmente en la comunidad científica debido al gran número de librerías disponibles para trabajar en proyectos de ciencia de datos y inteligencia artificial.

Aunque todo lo anterior es cierto, no hay que olvidar que Java esta muy presente en el mundo empresarial, así que no es raro que en algún momento sea necesario ejecutar Python desde una aplicación desarrollada en Java.

En este post, se mostrarán algunas de las opciones mas usadas para ejecutar código Python desde Java.

2. Código Python de ejemplo

En este post se va usar un simple script en Python que guardaremos en el archivo hola.py:

print("Hola Mundo!")

Asumo que ya tienes alguna versión instalada de Python, así que ejecutamos el script de la siguiente manera:

$ python hola.py 
Hola Mundo!

3. Core Java

En esta sección, vamos a ver dos opciones diferentes en las que podemos invocar un script en Python desde código Java.

3.1 Usando ProcessBuilder

La primera opción es a través del uso de ProcessBuilder API que nos permite crear un proceso nativo del sistema operativo para lanzar Python y ejecutar nuestro script.

public void usoProcessBuilder() throws Exception {
    ProcessBuilder processBuilder = new ProcessBuilder("python", resolvePythonScriptPath("hola.py"));
    processBuilder.redirectErrorStream(true);

    Process process = processBuilder.start();
    List<String> results = readProcessOutput(process.getInputStream());
    
    int exitCode = process.waitFor();    
}

En esta opción, estamos ejecutando el comando python y cuyo único argumento es nuestro script hola.py.

Para ello creamos un objeto ProcessBuilder al que se le pasa el comando y el argumento en el constructor del objeto. Es importante mencionar la llamada a redirectErrorStream(true). En el caso de que se produzcan errores, la salida de esos errores está fusionada con la salida estándar (stdin).

Esta llamada a redirectErrorStream(true) es muy útil ya que vamos a obtener cualquier error que se produzca en la llamada por la salida estándar. Si no establecemos la propiedad true en la llamada, será necesario leer la salida de estos errores por separado, lo que obliga a tener 2 canales: el getInputStream() y el getErrorStream().

Una vez aclarado esto, podemos ejecutar el proceso usando el método start() para obtener un objeto Process. Este objeto nos proporcionará la salida (stdin) usando el canal getInputStream().

Como he mencionado anteriormente, asumo que tienes una instalación válida de Python y que el comando python esta definido en el PATH.

3.2 Usando la especificación JSR-223 (Scripting Engine)

La especificación JSR-223, que se introdujo en Java 6, define una serie de API’s que proporcionan funcionalidades básicas de “scripting” en Java. Estos métodos proporcionan mecanismos para ejecutar scripts y compartir valores entre Java y el lenguaje de “scripting”. El objetivo principal de este estándar es proporcionar una forma uniforme para que Java pueda interactuar con los lenguajes de “scripting”.

Para usar este estándar es necesario que el sistema de scripting que vayamos a usar use la arquitectura de scripting proporcionada por la implementación JVM. En el caso de Python, tenemos Jython que es un implementación de Java que fuciona sobre la JVM.

Asumiendo que tenemos Jython en nuestro CLASSPATH, la framework debería detectar que tenemos la posibilidad de usar esta “scripting engine” para realizar las llamadas de código Python desde Java.

Puedes obtener Jython desde el repositorio de Maven Central, y incluirlo en tu pom.xml de la siguiente manera:

<dependency>
    <groupId>org.python</groupId>
    <artifactId>jython-slim</artifactId>
    <version>2.7.3b1</version>
</dependency>

También se puede descargar y instalarlo directamente.

Una nota importante: Jython de momento solo puede ejecutar Python 2+ y no hay fecha anunciada para la compatibilidad con Python 3. Esta limitación es importante ya que la mayoría de las librerías interesantes en Python son 3+.

Una vez que hemos incluido Jython en nuestro CLASSPATH, vamos a listar todas las “scripting engines” que tenemos disponibles añadiendo el siguiente código:

ScriptEngineManagerUtils.listEngines();

Si hemos añadido la librería correctamente y tenemos la posibilidad de ejecutar Jython veremos la siguiente descripción en la salida el código anterior:

...
Engine name: jython
Version: 2.7.2
Language: python
Short Names:
python
jython

Ahora que ya tenemos disponible el uso de Jython como “scripting engine”, vamos a realizar una llamada al script hola.py usando el siguiente código:

public void usoJython() throws Exception {
    StringWriter writer = new StringWriter();
    ScriptContext context = new SimpleScriptContext();
    context.setWriter(writer);

    ScriptEngineManager manager = new ScriptEngineManager();
    ScriptEngine engine = manager.getEngineByName("python");
    engine.eval(new FileReader(resolvePythonScriptPath("hola.py")), context);    
}

Como podemos ver, es muy simple trabajar con esta API. Primero, establecemos el contexto usando ScriptContext que contiene un StringWriter. Este objeto va a almacenar la salida de nuestro script.

Una vez especificado el contexto, usamos el método getEngineByName(“python”) para obtener el “ScriptEngine” que nos permitirá invocar las llamadas a nuestro código Python.. En este caso podemos pasar “python” o “jython” que son los dos nombres admitidos por esta “engine”.

Como en el anterior ejemplo, el paso final es obtener la salida del script.

4. Jython

Continuando con lo que hemos visto de Jython, también tenemos la posibilidad de añadir código Python en nuestro código Java. Para realizar esto podemos usar la clase PythonInterpreter:

public void pythonInterpreter() {
    try (PythonInterpreter pyInterp = new PythonInterpreter()) {
        StringWriter output = new StringWriter();
        pyInterp.setOut(output);

        pyInterp.exec("print('Hola mundo!')");        
    }
}

El uso de la clase PythonInterpreter nos permite ejecutar código python definido en un String usando el método exec. Como hemos visto anteriormente podemos capturar la salida usando StringWriter.

Vamos a ver otro ejemplo simple en el que vamos a realizar el sumatorio de dos números:

public void pythonInterpreterSuma2() {
    try (PythonInterpreter pyInterp = new PythonInterpreter()) {
        pyInterp.exec("x = 20+30");
        PyObject x = pyInterp.get("x");        
    }
}

En este ejemplo podemos ver que usando el método get podemos acceder al valor de una variable de python.

A continuación, vamos a realizar un último ejemplo para ver que ocurre cuando se produce un error:

try (PythonInterpreter pyInterp = new PythonInterpreter()) {
    pyInterp.exec("import o2sa");
}

Cuando ejecutamos el anterior código se lanza una PyException y podemos ver el error como si estuviéramos ejecutando el código en el REPL de Python:

Traceback (most recent call last):
  File "<string>", line 1, in <module>
ImportError: No module named o2sa

Algunos puntos a considerar:

  • Ya que PythonInterpreter implementa la interface AutoCloseable es una buena práctica usar try-with-resources cuando trabajemos con esta clase.

  • El nombre de la clase PythonInterpreter no implica que nuestro código Python sea interpretado. Los scripts en Python usando Jython son ejecutados por la JVM y compilados a código bytecode Java antes de su ejecución.

  • Aunque Jython es una implementación de Python en Java, podría no contener todas las librerías que nos ofrece la versión nativa de Python.

5. Apache Commons Exec

Otra librería que podemos para realizar llamadas de código Python desde java es Apache Common Exec que intenta mitigar los problemas que se producen al usar Java Process API.

El artifact de esta librería esta disponible en Maven Central:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-exec</artifactId>
    <version>1.3</version>
</dependency>

A continuación podemos ver un ejemplo de como usar esta librería para realizar la llamada de una forma muy similar a la que hemos visto anteriormente usando la API ProcessBuilder:

public void apacheCommonExec() 
  throws ExecuteException, IOException {
    String line = "python " + resolvePythonScriptPath("hola.py");
    CommandLine cmdLine = CommandLine.parse(line);
        
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream);
        
    DefaultExecutor executor = new DefaultExecutor();
    executor.setStreamHandler(streamHandler);

    int exitCode = executor.execute(cmdLine);    
}

Este ejemplo no es muy diferente al primer ejemplo que vimos usando ProcessBuilder. Como podemos observar, primero creamos un objeto CommandLine para la ejecución de nuestro comando. A continuación configuramos un handler para el stream que va a capturar la salida de nuestro proceso.

6. Conclusión

Para ejecutar código Python desde Java, disponemos de varias opciones que nos permiten hacerlo de forma sencilla y eficiente. Jython, Apache Commons Exec y ProcessBuilder son las mejores opciones para ejecutar código python desde Java. Aunque cada una de estas opciones tiene sus propias ventajas e inconvenientes, lo importante evaluar cuál de ellas se adapta mejor a las necesidades del proyecto.

Enlaces y vídeos de interés

Artículos

Vídeos



Posts que te pueden interesar:


LinkedIn

comments powered by Disqus