Hlavní navigace

Komunikace mezi Pythonem a Javou s využitím nástroje py4j

29. 4. 2021
Doba čtení: 27 minut

Sdílet

 Autor: Python
V dnešním článku si popíšeme potenciálně velmi užitečný projekt nazvaný py4j. Tento nástroj zajišťuje obousměrnou komunikaci na úrovni zdrojového kódu mezi Pythonem a programovacím jazykem Java.

Obsah

1. Komunikace mezi Pythonem a Javou s využitím nástroje py4j

2. Propojení staticky typovaného a kompilovaného jazyka s jazykem skriptovacím

3. Instalace projektu py4j

4. Příprava brány na straně JVM

5. Překlad a spuštění brány – nastavení proměnné prostředí CLASSPATH

6. Port, který brána používá ve výchozím nastavení

7. Zavolání metody definované v Javě z Pythonu

8. Aplikace s několika otevřenými branami

9. Zavolání metod přes různé vstupní body z Pythonu

10. Volání metod s parametry a návratovými hodnotami různých typů

11. Přístup k metodám s různými parametry z Pythonu

12. Sdílení datových struktur mezi Pythonem a Javou

13. Využití mapy přečtené z Javy

14. Vybrané další možnosti nabízené projektem py4j

15. Alternativní řešení propojení Pythonu s ekosystémem Javy

16. JPype

17. Python na GraalVM

18. Jython

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

1. Komunikace mezi Pythonem a Javou s využitím nástroje py4j

Obor informatiky se, ostatně podobně jako i prakticky všechny další obory, potýká s problémy, které vyplývají z nedostatečné standardizace a unifikace. Tento stav nemusí být nutně špatný, protože může vést (a mnohdy taktéž vede) k rychlejšímu vývoji nových technologií, ovšem na druhou stranu dochází k situacím, kdy je například nutné (či alespoň vhodné) propojit například dva ekosystémy, jenž se vyvíjely nezávisle na sobě. Příkladem, kterým se budeme zabývat v dnešním článku, je dvojice rozsáhlých a taktéž propracovaných ekosystémů. První ekosystém je postaven okolo programovacího jazyka Java, či spíše okolo jeho virtuálního stroje (který má podle mého názoru v současnosti již mnohem větší význam, než samotný jazyk). A druhý ekosystém je postaven okolo Pythonu. V obou ekosystémech se mnohdy řeší stejné problémy, ovšem jinými nástroji: Maven/pip, CLASSPATH a class loadery/virtuální prostředí, profily, JNI/FFI atd. (a jak je již z tohoto porovnání patrné, nejedná se mnohdy o náhrady 1:1).

Přitom je mnohdy žádoucí, aby bylo umožněno v projektech využívat jak programovací jazyk Python, tak i Javu, popř. jiné jazyky postavené nad virtuálním strojem Javy. Samozřejmě je možné zajistit komunikaci například na bázi REST API, systému front, popř. systému publish-subscribe (MQTT apod.), ovšem pro programátory je jednodušší přímá komunikace mezi programovým kódem napsaným v Pythonu a kódem vytvořeným v Javě. A jedno z řešení je nabízeno právě projektem py4j, jenž umožňuje obousměrnou komunikaci mezi Pythonem a Javou. Nutno ovšem poznamenat, že existují i další řešení postavené na odlišných technologiích. Příkladem může být Python pro GraalVM, což je velmi užitečný projekt představený v sedmnácté kapitole. A druhým příkladem je Jython, tedy reimplementace jazyka Python pro JVM. S tímto projektem jsme se již na stránkách Roota setkali, takže si jeho existenci pouze krátce připomeneme v kapitole číslo 18.

Díky projektu py4j může skript napsaný v Pythonu přistupovat ke třídám, objektům a atributům vytvořeným v Javě a naopak. Skript v Pythonu přitom běží nad klasickým CPythonem (možná bude funkční i v PyPy) a kód psaný v Javě je zkompilován do standardního bajtkódu JVM a spuštěn nad JVM. Komunikace probíhá s využitím protokolu TCP, a to při výchozím nastavení pouze na lokální úrovni. Podrobnosti o použitých portech a o možnosti komunikace s větším množstvím JVM (například) budou zmíněny v navazujících kapitolách.

Tip: Root.cz pořádá vlastní kurzy programování v jazyce Java

2. Propojení staticky typovaného a kompilovaného jazyka s jazykem skriptovacím

„Interview Guido van Rossum: “I'd rather write code than papers.”“

Z obecného pohledu je možné říci, že kombinace striktně typovaného a většinou i překládaného programovacího jazyka s jazykem dynamicky typovaným, jenž se používá ve formě „lepidla“ (glue), bývá i jen u mírně rozsáhlejších systémů velmi úspěšná. Jako typický příklad se mnohdy uvádí samotný koncept UNIXu a tedy i klasického Linuxu – jádro, knihovny i základní nástroje jsou naprogramovány v typovaném a překládaném jazyku (i když z historických důvodů nikoli v silně typovaném jazyku), zatímco jako „lepidlo“ slouží skripty psané v nějakém shellu. Navíc je rozhraní UNIXu navrženo takovým způsobem, že není dopředu stanoveno, o jaký shell se musí jednat. Typicky se sice jedná o CSH, BASH, KSH apod., ovšem stejně dobře by bylo možné použít například jazyk Rexx, s mírnými problémy Python, Babashku atd. atd. Ovšem skriptovací jazyky, resp. rozhraní pro ně, jsou součástí i mnoha dalších úspěšných aplikací, od Microsoft Office přes AutoCAD a GIMP po (řekněme) Blender.

Nejpoužívanějším vysokoúrovňovým dynamicky typovaným jazykem je v současnosti programovací jazyk Python, jenž vznikl na samotném začátku devadesátých let minulého století. Z mnoha pohledů se jednalo o důležitý mezník v rozvoji IT, protože právě tehdy se začala stále více rozšiřovat myšlenka, že programovací jazyky určené pro vývoj plnohodnotných aplikací lze zhruba rozdělit do dvou kategorií – překládané systémové jazyky a jazyky skriptovací. Samozřejmě, že se skriptovací jazyky používaly i před tímto obdobím, ale většinou se jednalo o relativně primitivní formy předpisů pro dávkové úlohy (výjimkou je například již zmíněný jazyk Rexx, jehož vyjadřovací prostředky již byly na vysoké úrovni) a převažoval názor, že plnohodnotné aplikace musí být psány v překládaných jazycích, tedy typicky v jazycích ALGOLské větvě se statickým typováním (schválně nepíšu se silným typováním, to je sice související, ovšem odlišná vlastnost).

V průběhu devadesátých let se tedy zpočátku mírně opovrhované skriptovací jazyky staly mnohdy nedílnou součástí mnoha profesionálních aplikací. Celý vývoj a s ním související myšlenkový posun byl nakonec shrnut ve slavném článku Johna Ousterhouta „Scripting: Higher Level Programming for the 21st Century“, v němž se opakovala myšlenka na souběžné a koopertivní použití dvou jazyků – systémového a skriptovacího.

3. Instalace projektu py4j

Před vyzkoušením možností, které jsou nabízeny projektem py4j samozřejmě musíme tento projekt nainstalovat. K tomuto účelu se používá standardní pythonovský správce balíčků pip, resp. pip3. Nenechte se ovšem zmýlit, protože pip ve skutečnosti nenainstaluje pouze část určenou přímo pro jazyk Python, ale i Java archiv (neboli JAR) obsahující tu část py4j, která musí běžet v JVM. Tento Java archiv je poněkud „skrytý“, což bude později vyžadovat modifikaci proměnné prostředí CLASSPATH, popř. přesun tohoto Java archivu na jiné místo (postačuje samozřejmě pouze vytvoření symbolického odkazu – symlinku).

Poznámka: to, že pip dokáže kromě kódu napsaného v Pythonu instalovat i další knihovny, Java archivy atd. vlastně není nic překvapivého, protože i mnohé další balíčky pro jazyk Python vyžadují nativní části, typicky překládané s využitím C či C++.

Vraťme se nyní k instalaci py4j. Ta se provede tímto příkazem:

$ pip3 install --user py4j
 
Collecting py4j
  Using cached https://files.pythonhosted.org/packages/30/42/25ad191f311fcdb38b750d49de167abd535e37a144e730a80d7c439d1751/py4j-0.10.9.1-py2.py3-none-any.whl
    100% |████████████████████████████████| 204kB 1.3MB/s
Installing collected packages: py4j
Successfully installed py4j-0.10.9.1

Po provedení tohoto příkazu by měla být v Pythonu přímo dostupná pythonovská část projektu py4j, což si ostatně můžeme ihned otestovat v interaktivní smyčce REPL Pythonu:

>>> from py4j.java_gateway import JavaGateway
 
>>> help(JavaGateway)
 
Help on class JavaGateway in module py4j.java_gateway:
 
class JavaGateway(builtins.object)
 |  A `JavaGateway` is the main interaction point between a Python VM and
 |     a JVM.
 |
 |  * A `JavaGateway` instance is connected to a `Gateway` instance on the
 |    Java side.
 |
 |  * The `entry_point` field of a `JavaGateway` instance is connected to
 |    the `Gateway.entryPoint` instance on the Java side.
 |
 |  * The `java_gateway_server` field of a `JavaGateway` instance is connected
 |    to the `GatewayServer` instance on the Java side.
 |
 |  * The `jvm` field of `JavaGateway` enables user to access classes, static
 |    members (fields and methods) and call constructors.
 |
 |  * The `java_process` field of a `JavaGateway` instance is a
 |    subprocess.Popen object for the Java process that the `JavaGateway`
 |    is connected to, or None if the `JavaGateway` connected to a preexisting
 |    Java process (in which case we cannot directly access that process from
 |    Python).

Současně by mělo dojít k instalaci již výše zmíněného Java archivu, který byl na mém systému umístěn do adresáře ~/.local/share/py4j:

$ ls -l ~/.local/share/py4j
 
total 128
-rw-rw-r--. 1 ptisnovs ptisnovs 121370 Feb 21 09:21 py4j0.10.9.1.jar
Poznámka: toto umístění se ovšem může odlišovat.

4. Příprava brány na straně JVM

Vyzkoušejme si nyní, jakým způsobem je vlastně navázána komunikace mezi Pythonem na jedné straně a virtuálním strojem programovacího jazyka Java na straně druhé. Nejdříve vytvoříme jednoduchý projekt v Javě, který bude mj. sloužit i jako server, k němuž se následně připojíme z Pythonu. V tomto projektu je definován takzvaný vstupní bod (entry point), jenž mj. definuje objekty, metody atd. dosažitelné z Pythonu. Dále je spuštěn server (který se nazývá gateway):

import py4j.GatewayServer;
 
public class Gateway1 {
 
    public static void main(String[] args) {
        System.out.println("Starting gateway server");
        GatewayServer gatewayServer = new GatewayServer(new Gateway1());
        gatewayServer.start();
        System.out.println("gateway server started");
    }
 
}
Poznámka: povšimněte si, že konstruktoru třídy GatewayServer je předána instance (libovolné) třídy, která tvoří vstupní bod (entry point) pro Python. V dalším textu si ukážeme, že je možné předat i jiný objekt – v tomto příkladu totiž může dojít k nepochopení toho, že třída Gateway1 sice obsahuje statickou metodu main, což ovšem nijak nesouvisí či nemusí souviset se vstupním bodem.

5. Překlad a spuštění brány – nastavení proměnné prostředí CLASSPATH

Při překladu je nutné na CLASSPATH přidat i Java archiv s implementací py4j. Pokud je tento archiv v aktuálním adresáři (což většinou nebude):

$ javac -cp py4j0.10.9.1.jar Gateway1.java

Samozřejmě můžete specifikovat celou cestu ke zmíněnému Java archivu:

$ javac -cp ~/.local/share/py4j/py4j0.10.9.1.jar Gateway1.java

Popř. je možné nastavit proměnnou prostředí CLASSPATH, což je možná nejrozumnější řešení, neboť tuto proměnnou můžete měnit například i z integrovaných vývojových prostředí atd.:

$ export CLASSPATH=~/.local/share/py4j/py4j0.10.9.1.jar:$CLASSPATH
$ javac Gateway1.java

Stejně je tomu při spuštění aplikace. V případě, že na CLASSPATH není nalezen Java archiv s implementací py4j, dojde k chybě při pokusu o inicializaci aplikace:

$ java Gateway1
 
Starting gateway server
Exception in thread "main" java.lang.NoClassDefFoundError: py4j/GatewayServer
        at Gateway1.main(Gateway1.java:7)
Caused by: java.lang.ClassNotFoundException: py4j.GatewayServer
        at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
        ... 1 more

Spuštění s explicitním nastavením CLASSPATH:

$ java -cp ~/.local/share/py4j/py4j0.10.9.1.jar:. Gateway1

Spuštění s upravenou proměnnou prostředí CLASSPATH:

$ export CLASSPATH=~/.local/share/py4j/py4j0.10.9.1.jar:$CLASSPATH
$ java Gateway1

Po spuštění by se měla vypsat zpráva o tom, že byl spuštěn server představující bránu (gateway) pro Pythonovskou část aplikace:

Starting gateway server
gateway server started

6. Port, který brána používá ve výchozím nastavení

V úvodních kapitolách jsme si řekli, že pro komunikaci mezi Pythonem a JVM se používá protokol TCP. Zajímavé tedy bude zjistit, jaké porty jsou po spuštění javovské části obsazeny. K tomuto účelu použijeme nástroj netstat a necháme si vypsat servery využívající TCP. Aktivní port vytvořené brány je zvýrazněn tučným písmem:

$ netstat -lt
 
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 localhost:ircu-3        0.0.0.0:*               LISTEN
tcp        0      0 0.0.0.0:sunrpc          0.0.0.0:*               LISTEN
tcp        0      0 localhost:domain        0.0.0.0:*               LISTEN
tcp        0      0 localhost.locald:domain 0.0.0.0:*               LISTEN
tcp        0      0 localhost.locald:domain 0.0.0.0:*               LISTEN
tcp        0      0 0.0.0.0:ssh             0.0.0.0:*               LISTEN
tcp6       0      0 [::]:sunrpc             [::]:*                  LISTEN
tcp6       0      0 localhost:25333         [::]:*                  LISTEN
tcp6       0      0 [::]:ssh                [::]:*                  LISTEN
Poznámka: jak uvidíme dále, je tento port plně konfigurovatelný.

Vypsat si můžeme i proces, který daný server spustil:

$ netstat -tlp
 
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 localhost:ircu-3        0.0.0.0:*               LISTEN      -
tcp        0      0 0.0.0.0:sunrpc          0.0.0.0:*               LISTEN      -
tcp        0      0 localhost:domain        0.0.0.0:*               LISTEN      -
tcp        0      0 localhost.locald:domain 0.0.0.0:*               LISTEN      -
tcp        0      0 localhost.locald:domain 0.0.0.0:*               LISTEN      -
tcp        0      0 0.0.0.0:ssh             0.0.0.0:*               LISTEN      -
tcp6       0      0 [::]:sunrpc             [::]:*                  LISTEN      -
tcp6       0      0 localhost:25333         [::]:*                  LISTEN      21828/java
tcp6       0      0 [::]:ssh                [::]:*                  LISTEN      -

Informaci o tom, jaké aplikaci odpovídá proces s PID 21828 nám podá nástroj jps:

$ jps
 
29362 Jps
21828 Gateway1

7. Zavolání metody definované v Javě z Pythonu

Nyní si ukažme způsob volání metody definované v Javě ze skriptu napsaného v Pythonu. Vytvoříme si nejprve novou javovskou aplikaci, která bude mj. obsahovat i metodu nazvanou getMessage, která je bez parametrů, ovšem vracející řetězec. Vstupním bodem bude instance třídy Gateway2, ve které je metoda getMessage deklarována:

import py4j.GatewayServer;
 
public class Gateway2 {
 
    public String getMessage() {
        return "Hello from Java!";
    }
 
    public static void main(String[] args) {
        System.out.println("Starting gateway server");
        GatewayServer gatewayServer = new GatewayServer(new Gateway2());
        gatewayServer.start();
        System.out.println("gateway server started");
    }
 
}

Tuto část přeložíme a spustíme naprosto stejným způsobem, jako první aplikaci.

Poznámka: pokud první aplikace stále poběží, vypíše se chyba informující o tom, že port 25333 je již obsazený.

Pythonovský skript bude vypadat takto:

from py4j.java_gateway import JavaGateway
 
gateway = JavaGateway()
 
message = gateway.entry_point.getMessage()
print(message)
 
input("Press Enter to continue...")

Ve skriptu je zkonstruován objekt typu JavaGateway, který zajistí komunikaci s JVM. Ihned poté již můžeme zavolat metodu getMessage, a to takovým způsobem, jakoby se jednalo o metodu objektu gateway.entry_point. Výsledkem bude standardní Pythonovský řetězec – i když interně muselo dojít k převodům, neboť řetězce v Pythonu jsou uloženy zcela odlišným způsobem, než je tomu v Javě (viz též Interní reprezentace řetězců v různých jazycích: od počítačového pravěku po současnost.

8. Aplikace s několika otevřenými branami

Díky tomu, že komunikace mezi Pythonem (resp. přesněji řečeno aplikací naprogramovanou v Pythonu) a virtuálním strojem jazyka Java probíhá přes protokol TCP (každá strana zde vystupuje v roli serveru a současně i klienta), je možné poměrně dynamicky měnit propojení Python → Java a naopak. V praxi to například znamená, že jeden skript naprogramovaný v Pythonu se může připojovat k více virtuálním strojům Javy a postupně či dokonce v jeden okamžik s nimi komunikovat. A podobně lze zajistit, aby aplikace psaná v Javě byla ovládána více pythonovskými skripty, pokaždé na jiném portu a tedy nezávisle na sobě. Musíme samozřejmě zajistit, aby spolu komunikovaly ty správné části aplikace – a přesně k tomu slouží volba portů. Standardně jsou používány dva porty. Port 25333 je otevírán na straně JVM a připojuje se k němu skript napsaný v Pythonu. A naopak port 25332 je otevírán na straně Pythonu a používá se pro komunikaci ze strany javovské aplikace.

Podívejme se nyní na způsob volby portů. Upravíme nejdříve tu stranu aplikace, která je naprogramovaná v Javě. Volba portu se provádí při konstrukci brány:

GatewayServer gatewayServer1 = new GatewayServer(new EntryPoint1(), 20001);
gatewayServer1.start();

Upravená aplikace otevře dva porty a pro každý port použije vlastní gateway, přičemž každá z těchto bran bude mít nakonfigurován jiný přístupový bod neboli entry point. Zde tedy opět můžeme vidět značnou flexibilitu použitého řešení:

import py4j.GatewayServer;
 
class EntryPoint1 {
    public String getMessage() {
        return "Hello from entrypoint #1";
    }
}
 
class EntryPoint2 {
    public String getMessage() {
        return "Hello from entrypoint #2";
    }
}
 
public class Gateway3 {
 
    public static void main(String[] args) {
        System.out.println("Starting two gateway servers");
 
        GatewayServer gatewayServer1 = new GatewayServer(new EntryPoint1(), 20001);
        gatewayServer1.start();
 
        GatewayServer gatewayServer2 = new GatewayServer(new EntryPoint2(), 20002);
        gatewayServer2.start();
 
        System.out.println("gateway servers started");
    }
 
}

Nyní tento demonstrační příklad běžným způsobem přeložíme a spustíme (již známým způsobem).

Zajímavé bude zjistit, jaké porty jsou nyní obsazeny. K tomuto účelu opět použijeme nástroj netstat. Aktivní porty obou bran jsou zvýrazněny tučným písmem:

$ netstat -ntl
 
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 127.0.0.1:6667          0.0.0.0:*               LISTEN
tcp        0      0 0.0.0.0:111             0.0.0.0:*               LISTEN
tcp        0      0 127.0.0.1:53            0.0.0.0:*               LISTEN
tcp        0      0 192.168.122.1:53        0.0.0.0:*               LISTEN
tcp        0      0 192.168.130.1:53        0.0.0.0:*               LISTEN
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN
tcp6       0      0 127.0.0.1:20001         :::*                    LISTEN
tcp6       0      0 127.0.0.1:20002         :::*                    LISTEN
tcp6       0      0 :::111                  :::*                    LISTEN
tcp6       0      0 :::22                   :::*                    LISTEN

9. Zavolání metod přes různé vstupní body z Pythonu

Na straně skriptu napsaného v Pythonu je situace opačná, protože (alespoň prozatím) budeme volat metody naprogramované v Javě a přístupné přes přístupový bod. Nejdříve se připojíme k první bráně (gateway) a zavoláme přes ni metodu getMessage():

gateway1 = JavaGateway(gateway_parameters=GatewayParameters(port=20001))
message = gateway1.entry_point.getMessage()
print(message)

Posléze použijeme druhou bránu a opět zavoláme metodu getMessage(); ovšem v tuto chvíli se pochopitelně jedná o odlišnou metodu:

gateway2 = JavaGateway(gateway_parameters=GatewayParameters(port=20002))
message = gateway2.entry_point.getMessage()
print(message)
Poznámka: zde je patrné, že volba portu v Pythonu je nepatrně složitější, než je tomu v případě javovské strany aplikace (je nutné použít objekt GatewayParameters).

Úplný tvar skriptu vypadá následovně:

from py4j.java_gateway import JavaGateway, GatewayParameters
 
gateway1 = JavaGateway(gateway_parameters=GatewayParameters(port=20001))
gateway2 = JavaGateway(gateway_parameters=GatewayParameters(port=20002))
 
message = gateway1.entry_point.getMessage()
print(message)
 
message = gateway2.entry_point.getMessage()
print(message)
 
input("Press Enter to continue...")

Otestujeme, že se skutečně zavolají dvě různé metody, které mají společnou jen tu vlastnost, že se jmenují stejně:

$ python3 UseGateway3.py 
 
Hello from entrypoint #1
Hello from entrypoint #2
Press Enter to continue...

10. Volání metod s parametry a návratovými hodnotami různých typů

Prozatím jsme si ukázali jen způsob zavolání metody deklarované v Javě ze skriptu naprogramovaného v Pythonu, a to včetně zpracování návratové hodnoty této metody. Ve skutečnosti jsou ovšem možnosti poskytované nástrojem py4j mnohem větší, protože je například možné zpracovat kolekci vytvořenou v Javě, používat celé třídy, pracovat s výjimkami atd. Nejdříve si ukážeme způsob volání metod s předáváním parametrů různých typů a zpracováním návratových hodnot (taktéž různých typů). V dalším demonstračním příkladu, resp. přesněji řečeno v jeho javovské části, je připraveno několik metod volatelných z Pythonu:

import py4j.GatewayServer;
 
import java.util.*;
 
class EntryPoint1 {
    public int add(int x, int y) {
        return x+y;
    }
 
    public double fadd(double x, double y) {
        return x+y;
    }
 
    public String sadd(String x, String y) {
        return x+y;
    }
 
    public List<String> aList(String first, String second) {
        List<String> list = new ArrayList<String>();
        list.add(first);
        list.add(second);
        return list;
    }
 
    public Map<String, String> aMap(String key, String value) {
        Map<String, String> map = new HashMap<String, String>();
        map.put(key, value);
        map.put("foo", "bar");
        map.put("bar", "baz");
        return map;
    }
 
    public String getMessage() {
        return "Hello from entrypoint #1";
    }
}
 
public class Gateway4 {
 
    public static void main(String[] args) {
        System.out.println("Starting gateway server");
 
        GatewayServer gatewayServer1 = new GatewayServer(new EntryPoint1(), 20001);
        gatewayServer1.start();
 
        System.out.println("gateway server started");
    }
 
}

Tento příklad přeložíme a spustíme naprosto stejným způsobem, jako příklady předchozí, tedy následovně:

$ export CLASSPATH=~/.local/share/py4j/py4j0.10.9.1.jar:$CLASSPATH
$ javac Gateway4.java
$ java Gateway4

11. Přístup k metodám s různými parametry z Pythonu

Nyní se podívejme na skript vytvořený v Pythonu. Ten je schopný bez většího programátorského úsilí zavolat javovské metody akceptující různé typy parametrů a zpracovat návratové hodnoty těchto metod, a to opět bez větších problémů (ovšem interně je situace poměrně složitá a py4j musí provádět různé typové konverze atd.):

from py4j.java_gateway import JavaGateway, GatewayParameters
 
gateway = JavaGateway(gateway_parameters=GatewayParameters(port=20001))
 
message = gateway.entry_point.getMessage()
print(message)
 
print(gateway.entry_point.add(10, 20))
print(gateway.entry_point.fadd(10.0, 20.0))
 
aString = gateway.entry_point.sadd("foo", "bar")
print(aString)
print(type(aString))
print()
 
aList = gateway.entry_point.aList("first", "second")
print(aList)
print(type(aList))
print()
 
aMap = gateway.entry_point.aMap("key", "value")
print(aMap)
print(type(aMap))
print()
 
input("Press Enter to continue...")

Výsledek běhu tohoto skriptu:

Hello from entrypoint #1
30
30.0
foobar
<class 'str'>
 
['first', 'second']
<class 'py4j.java_collections.JavaList'>
 
{'bar': 'baz', 'foo': 'bar', 'key': 'value'}
<class 'py4j.java_collections.JavaMap'>
 
Press Enter to continue...

12. Sdílení datových struktur mezi Pythonem a Javou

Kromě volání metod z JVM (či naopak funkcí a metod deklarovaných v Pythonu) je možné i sdílení datových struktur, což je obecně komplikované téma, neboť jak v Javě, tak i v Pythonu jsou výchozí datové struktury měnitelné (mutable) a současně nejsou perzistentní. To znamená, že například modifikace datové struktury provedená v Javě se musí promítnout i do Pythonu a pochopitelně i naopak. Tuto vlastnost si ukážeme v pořadí již pátém demonstračním příkladu, v němž je na straně Javy vytvořena mapa (asociativní pole), kterou je možné přečíst metodou pojmenovanou getMap a vytisknout metodou printMap. Ovšem mapa je nabízena Pythonovské části kódu přímo (bez klonování), což znamená, že změny v mapě provedené na straně Pythonu budou viditelné i z Javy (takže se interně musí provést určitá komunikace):

import py4j.GatewayServer;
 
import java.util.*;
 
class EntryPoint1 {
 
    Map<String, String> aMap = new HashMap<String, String>();
 
    public Map<String, String> getMap() {
        return this.aMap;
    }
 
    public void printMap() {
        System.out.println("Map seen on Java side:");
 
        for (Map.Entry<String, String> item: this.aMap.entrySet()) {
            System.out.format("key: %s,  value: %s\n", item.getKey(), item.getValue());
        }
 
        System.out.println();
    }
}
 
public class Gateway5 {
 
    public static void main(String[] args) {
        System.out.println("Starting gateway server");
 
        GatewayServer gatewayServer1 = new GatewayServer(new EntryPoint1(), 20001);
        gatewayServer1.start();
 
        System.out.println("gateway server started");
    }
 
}

Překlad tohoto příkladu probíhá stejně, jako u příkladů předchozích:

$ export CLASSPATH=~/.local/share/py4j/py4j0.10.9.1.jar:$CLASSPATH
$ javac Gateway5.java

13. Využití mapy přečtené z Javy

Ve skriptu naprogramovaném v Pythonu nejdříve navážeme připojení k JVM a získáme objekt představující vstupní bod. Poté metodou getMap přečteme mapu zkonstruovanou v Javě a zobrazíme ji v Pythonu. Následně je mapa upravena na straně Pythonu – je do ní přidán nový prvek a mapa je vytištěna, a to jak Pythonem, tak i Javou. A v posledním kroku je jeden prvek z mapy smazán a opět se vytiskne nová podoba mapy viditelné jak z Pythonu, tak i z Javy:

from py4j.java_gateway import JavaGateway, GatewayParameters
import pprint
 
gateway = JavaGateway(gateway_parameters=GatewayParameters(port=20001))
 
m = gateway.entry_point.getMap()
print("Original map:")
pprint.pprint(m)
print()
 
m["foo"] = "bar"
print("Updated map:")
pprint.pprint(m)
print()
 
print("Java side...")
gateway.entry_point.printMap()
print()
 
m["bar"] = "baz"
del m["foo"]
print("Updated map:")
pprint.pprint(m)
print()
 
print("Java side...")
gateway.entry_point.printMap()
print()
 
input("Press Enter to continue...")

Nejprve spustíme javovskou část:

$ java Gateway5
 
Starting gateway server
gateway server started

Poté je spuštěn skript napsaný v Pythonu, jenž provede všechny výše popsané operace:

$ python3 UseGateway5.py
 
Original map:
{}
 
Updated map:
{'foo': 'bar'}
 
Java side...
 
Updated map:
{'bar': 'baz'}
 
Java side...
 
Press Enter to continue...

Ve druhém terminálu se spuštěnou javovskou částí aplikace je patrné, že modifikace mapy se korektně propíše i do Javy:

Map seen on Java side:
key: foo,  value: bar
 
Map seen on Java side:
key: bar,  value: baz

14. Vybrané další možnosti nabízené projektem py4j

Ukažme si ještě některé další možnosti, které jsou projektem py4j nabízeny. V následujícím skriptu jsou tyto (vybrané) možnosti popsány v komentářích:

from py4j.java_gateway import JavaGateway, GatewayParameters
 
gateway = JavaGateway()
 
# zavolání konstruktoru Javovské třídy
# (zde není zapotřebí import)
o = gateway.jvm.java.lang.Object()
print(o)
 
# zavolání metody objektu
print(o.toString())
 
print()
 
# zavolání statické metody třídy
# (zde opět není zapotřebí import)
r = gateway.jvm.java.lang.Math.random()
print(r)
print()
 
# použití datové struktury ArrayList z Javy
lst = gateway.jvm.java.util.ArrayList()
 
lst.append("first")
lst.append("second")
 
# použít lze konstrukce známé z Pythonu při práci se seznamy
for i in range(1, 11):
    lst.append(i)
 
print(lst)
print()
 
# přístup k atributu třídy (statický atribut)
Pi = gateway.jvm.java.lang.Math.PI
e = gateway.jvm.java.lang.Math.E
print(Pi)
print(e)
print()
 
# import javovské třídy
from py4j.java_gateway import java_import
java_import(gateway.jvm,'java.util.*')
 
# pozor: rozdílné oproti předchozímu příkladu kvůli importu
lst = gateway.jvm.ArrayList()
 
lst.append("first")
lst.append("second")
 
for i in range(1, 11):
    lst.append(i)
 
print(lst)
print()
 
 
# vytvoření jednorozměrného pole
string_class = gateway.jvm.String
string_array = gateway.new_array(string_class, 5)
string_array[0] = "first"
string_array[1] = "second"
print(string_array[0])
print(string_array[1])
Poznámka: kvůli použití standardního portu tento skript můžete využít například oproti aplikaci Gateway1.

15. Alternativní řešení propojení Pythonu s ekosystémem Javy

Kromě projektu py4j vzniklo i několik dalších projektů, jejichž cílem je umožnění využití ekosystému programovacího jazyka Java z Pythonu. Tyto projekty je možné podle použité technologie rozdělit do dvou kategorií. První kategorii již známe – je to propojení běžného Pythonu (typicky CPythonu, popř. PyPi) s virtuálním strojem Javy s využitím vhodného komunikačního mechanismu. Do této kategorie spadá jak již popsaný py4j, tak i dále alespoň ve stručnosti zmíněný projekt nazvaný JPype (ovšem vlastní komunikace je řešena odlišnými prostředky). A do druhé kategorie lze zařadit implementaci Pythonu buď přímo pro klasický virtuální stroj Javy (Jython) nebo jeho úprava pro běh nad GraalVM, což je podle mého názoru technologie, která by se v budoucnu měla prosadit do větší míry, než je tomu v současnosti. Integrace do GraalVM je zmíněna v sedmnácté kapitole a v kapitole osmnácté si připomeneme existenci Jythonu.

16. JPype

JPype je nástroj, který se do určité míry podobá výše popsanému nástroji py4j, protože umožňuje, aby skripty naprogramované v Pythonu (a běžící ve vlastním standardním interpretru) měly přístup k aplikaci běžící ve vlastní JVM. Příkladem použití JPype je rychlá tvorba prototypů, což ostatně platí i pro py4j. Své použití ovšem tento nástroj může najít i například při testování atd. Podrobnosti o tomto nástroji budou uvedeny v samostatném článku, v němž budou pochopitelně uvedeny i příklady použití (podobné příkladům z dnešního článku).

17. Python na GraalVM

Zcela odlišný koncept v integraci Pythonu a programovacího jazyka Java, resp. celého ekosystému postaveného okolo Javy, je použit v projektu Pythonu pro GraalVM. Jedná se o úpravu Pythonu, resp. celého jeho runtime systému takovým způsobem, aby využil technologie použité v GraalVM. Výsledkem je interpret, který nejenže má přístup ke všem knihovnám jazyka Java, ale navíc je v porovnání s klasickým CPythonem rychlejší (přibližně na úrovni PyPy). Ukázku použití a taktéž benchmarky si opět ukážeme v samostatném článku.

Poznámka: tento projekt se sice oficiálně nachází v experimentální fázi, ale již je použitelný, a to prakticky bez větších problémů. Jedná se o technologii, která může mít velkou budoucnost – mohla by poněkud otřást světem běžných interpretrů.

18. Jython

Jython je jméno implementace programovacího jazyka Python určená pro běh ve virtuálním stroji jazyka Java (JVM – Java Virtual Machine). A nejenom to – aplikace psané v Jythonu mohou kooperovat s třídami a rozhraními vytvořenými v Javě, což je pro mnoho systémů velmi výhodné, protože s rostoucí složitostí moderních aplikací je většinou zapotřebí mít k dispozici vhodný skriptovací jazyk sloužící jako „lepidlo“ (glue) mezi jednotlivými bloky, z nichž se aplikace skládá (viz slavný a ve své době dosti provokující Ousterhoutův článek o skriptovacích jazycích, který byl zmíněn v úvodu). Jython ovšem samozřejmě není dokonalý. Jednou z jeho nevýhod je fakt, že je stále postaven na dnes již obstarožním Pythonu 2, druhou nevýhodou pak ta skutečnost, že se jedná o dosti pomalý jazyk. Tato pomalost se negativně projeví zejména při výpočtech a někdy i při manipulaci s rozsáhlými datovými strukturami, ovšem u aplikací, v nichž převládají I/O operace se nemusí jednat o kritický nedostatek.

Obrázek 1: Logo programovacího jazyka Jython.

Poznámka: na druhou stranu je nutné poznamenat, že v době, kdy Jython vznikl (pod jménem JPython již v roce 1999, pod novým jménem vydán před dvaceti lety, tedy v roce 2001), se kromě samotné Javy jednalo o nejpropracovanější programovací jazyk určený pro běh na JVM. I z toho důvodu byl relativně často využíván pro skriptování v různých enterprise systémech, například i ve WebSphere, Oracle WebLogicu atd. O to problematičtější se zdá být praktické zastavení vývoje Jythonu.

V úvodním odstavci jsme se zmínili o tom, že Jython je dosti pomalou variantou Pythonu. To je ostatně možné relativně snadno dokázat sadou benchmarků, které si dnes popíšeme pouze ve velké stručnosti, protože se nejedná o hlavní téma článku. Všechny benchmarky byly spuštěny v těchto interpretrech Pythonu:

  1. Jython 2.7.0
  2. Python 2.7.14 (dnes již zastaralý)
  3. Python 3.6.3 (dnes již zastaralý)

První benchmark provádí prakticky jen výpočty s výpisem výsledku výpočtů na standardní výstup. Ten je přesměrován do souboru, protože výsledkem výpočtů jsou bitmapy ve formátu Portable Pixel Map (viz [1]).

Obrázek 2: Výsledky prvního benchmarku vynesené do grafu (ostatní časy jsou v porovnání s Jythonem tak malé, že ani nejsou vyneseny).

Vzhledem k tomu, že Python podporuje i práci s komplexními čísly, si můžeme benchmark ještě více upravit, a to takovým způsobem, aby se v něm všechny výpočty prováděly právě nad typem complex. Zajímavé bude změření a porovnání rychlosti výpočtů, protože samotný virtuální stroj Javy primitivní typ „komplexní číslo“ nezná a tím pádem ani nepodporuje. Výsledkem bude tento zdrojový kód.

Obrázek 3: Výsledky druhého benchmarku vynesené do grafu.

Ve skutečnosti však mnoho v současnosti provozovaných aplikací neprovádí intenzivní výpočty s numerickými hodnotami, ale většina strojového času se stráví prováděním zcela odlišných operací. Typicky se zpracovávají řetězce, popř. se intenzivně pracuje s kolekcemi (v Pythonu typicky se seznamy, slovníky a množinami). Nesmíme zapomenout ani na serializaci a deserializaci dat (JSON, XML) tak typické pro webové služby, aplikace s grafickým uživatelským rozhraním či na přístup k databázím. Pojďme si tedy ukázat další dva odlišně pojaté benchmarky. Ve skutečnosti se jedná o takzvané „mikrobenchmarky“ zaměřené pouze na jedinou operaci, což je samozřejmě odlišné od reálných aplikací, ovšem pro základní porovnání mohou být i mikrobenchmarky použitelné (a to zejména ve chvíli, kdy naměřené hodnoty budou výrazně odlišné).

V pořadí již třetí benchmark je po implementační stránce skutečně velmi jednoduchý. Je v něm totiž deklarována funkce, které se předá celé kladné číslo n a výsledkem je řetězec obsahující znaky „0 1 2 … n“. Tento benchmark tedy – alespoň teoreticky – zkoumá rychlost provádění tří operací:

  1. Převod celého čísla na řetězec (provedeno celkem n-krát)
  2. Spojení (konkatenace) dvou řetězců (s kopií znaků druhého řetězce do řetězce prvního, opět provedeno n-krát)
  3. Činnost automatického správce paměti (garbage collector)

Výsledky získané po spuštění třetího benchmarku s využitím Jythonu, interpretru Pythonu 2 a interpretru Pythonu 3 jsou ukázány na grafu:

Obrázek 4: Výsledky třetího benchmarku (konkatenace řetězců) vynesené do grafu. Časy Jythonu jsou tak vysoké, že časy běhu Pythonu 2 a Pythonu 3 nejsou viditelné.

Čtvrtý a současně i poslední benchmark, s nímž se seznámíme, již nebude zaměřen ani na numerické výpočty ani na masivní práci s řetězci. Bude v něm implementován algoritmus pro nalezení všech prvočísel ve specifikovaném rozsahu. Konkrétně pro zjištění prvočísel použijeme tzv. Eratosthenovo síto, které slouží na zjištění všech hodnot, které NEjsou prvočísly. Zbylé hodnoty pochopitelně prvočísly budou. Tento algoritmus je možné implementovat mnoha různými způsoby. V našem konkrétním benchmarku s výhodou využijeme některé vlastnosti Pythonu: práci s množinami (tam se uloží hodnoty, které NEjsou prvočísly), použití generátorů (yield) a taktéž funkce range, která nám prakticky zadarmo vygeneruje všechny celočíselné násobky určité hodnoty.

Linux tip

Obrázek 5: Výsledky čtvrtého benchmarku (Eratosthenovo síto) vynesené do grafu.

19. Repositář s demonstračními příklady

Zdrojové kódy všech dnes popsaných demonstračních příkladů určených pro Python 3 a Javu od verze 1.8 byly uloženy do Git repositáře dostupného na adrese https://github.com/tisnik/most-popular-python-libs. V případě, že nebudete chtít klonovat celý repositář (ten je ovšem stále velmi malý, dnes má velikost zhruba několik desítek kilobajtů), můžete namísto toho použít odkazy na jednotlivé příklady, které naleznete v následující tabulce:

# Jméno souboru Stručný popis souboru Cesta
1 Gateway1.java první verze brány využitelné Pythonem https://github.com/tisnik/most-popular-python-libs/blob/master/py4j/Gateway1.java
       
2 Gateway2.java brána nabízející metodu getMessage volatelnou z Pythonu https://github.com/tisnik/most-popular-python-libs/blob/master/py4j/Gateway2.java
3 UseGateway2.py využití brány z Pythonu https://github.com/tisnik/most-popular-python-libs/blob/master/py4j/UseGateway2.py
       
4 Gateway3.java dvě brány s rozdílnými vstupními body https://github.com/tisnik/most-popular-python-libs/blob/master/py4j/Gateway3.java
5 UseGateway3.py využití obou bran z Pythonu https://github.com/tisnik/most-popular-python-libs/blob/master/py4j/UseGateway3.py
       
6 Gateway4.java brána s metodami různých typů akceptujících rozdílné typy parametrů https://github.com/tisnik/most-popular-python-libs/blob/master/py4j/Gateway4.java
7 UseGateway4.py využití nabízených metod z Pythonu https://github.com/tisnik/most-popular-python-libs/blob/master/py4j/UseGateway4.py
       
8 Gateway5.java práce s mapou sdílenou mezi Javou a Pythonem https://github.com/tisnik/most-popular-python-libs/blob/master/py4j/Gateway5.java
9 UseGateway5.py práce s mapou sdílenou mezi Javou a Pythonem https://github.com/tisnik/most-popular-python-libs/blob/master/py4j/UseGateway5.py
       
10 UseGateway6.py další možnosti poskytované projektem py4j https://github.com/tisnik/most-popular-python-libs/blob/master/py4j/UseGateway6.py

20. Odkazy na Internetu

  1. Welcome to Py4J
    https://www.py4j.org/index.html
  2. Getting Started with Py4J
    https://www.py4j.org/gettin­g_started.html
  3. py4j 0.10.9.2 na PyPi
    https://pypi.org/project/py4j/
  4. PATH and CLASSPATH
    https://docs.oracle.com/ja­vase/tutorial/essential/en­vironment/paths.html
  5. Modern High-Performance Python
    https://www.graalvm.org/python/
  6. Moving from Jython to GraalVM
    https://medium.com/graalvm/moving-from-jython-to-graalvm-cf52c4af6106
  7. Scripting: Higher Level Programmingfor the 21st Century
    https://users.ece.utexas.e­du/~adnan/top/ousterhout-scripting.pdf
  8. Rediscovering Ousterhout’s Dichotomy in the 21st Century while Developing and Deploying Software for Set-Theoretic Empirical Analysis:From R to Python/Qt to OCaml and Tcl/Tk
    https://www.tcl.tk/communi­ty/tcl2019/assets/talk167/Sli­des.pdf
  9. WebSphere Application Server Administration Using Jython
    https://www.informit.com/sto­re/websphere-application-server-administration-using-jython-9780137009527
  10. Introduction to the Python implementation for GraalVM
    https://medium.com/graalvm/how-to-contribute-to-graalpython-7fd304fe8bb9
  11. GraalVM: Python Quick Start
    https://www.graalvm.org/pyt­hon/quickstart/
  12. GraalVM Python: Interoperability
    https://github.com/oracle/gra­alpython/blob/master/docs/u­ser/Interoperability.md
  13. Py4j na Stack Overflow
    https://stackoverflow.com/tag­s/py4j/info
  14. Py4J 0.8.2.1 Released
    https://py4j.wordpress.com/
  15. PySpark
    https://databricks.com/glos­sary/pyspark
  16. PySpark Internals (Outdated)
    https://cwiki.apache.org/con­fluence/display/SPARK/PyS­park+Internals
  17. JPype
    http://jpype.sourceforge.net/in­dex.html
  18. JPype1 1.2.1 na PyPi
    https://pypi.org/project/JPype1/
  19. Interní reprezentace řetězců v různých jazycích: od počítačového pravěku po současnost
    https://www.root.cz/clanky/interni-reprezentace-retezcu-v-ruznych-jazycich-od-pocitacoveho-praveku-po-soucasnost/
  20. Příběhy z vývoje nejrychlejšího virtuálního stroje na světě
    https://www.root.cz/clanky/pribehy-z-vyvoje-nejrychlejsiho-virtualniho-stroje-na-svete/