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
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
19. Repositář s demonstračními příklady
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).
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
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"); } }
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
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.
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)
Ú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])
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.
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.
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:
- Jython 2.7.0
- Python 2.7.14 (dnes již zastaralý)
- 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í:
- Převod celého čísla na řetězec (provedeno celkem n-krát)
- Spojení (konkatenace) dvou řetězců (s kopií znaků druhého řetězce do řetězce prvního, opět provedeno n-krát)
- Č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.

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:
20. Odkazy na Internetu
- Welcome to Py4J
https://www.py4j.org/index.html - Getting Started with Py4J
https://www.py4j.org/getting_started.html - py4j 0.10.9.2 na PyPi
https://pypi.org/project/py4j/ - PATH and CLASSPATH
https://docs.oracle.com/javase/tutorial/essential/environment/paths.html - Modern High-Performance Python
https://www.graalvm.org/python/ - Moving from Jython to GraalVM
https://medium.com/graalvm/moving-from-jython-to-graalvm-cf52c4af6106 - Scripting: Higher Level Programmingfor the 21st Century
https://users.ece.utexas.edu/~adnan/top/ousterhout-scripting.pdf - 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/community/tcl2019/assets/talk167/Slides.pdf - WebSphere Application Server Administration Using Jython
https://www.informit.com/store/websphere-application-server-administration-using-jython-9780137009527 - Introduction to the Python implementation for GraalVM
https://medium.com/graalvm/how-to-contribute-to-graalpython-7fd304fe8bb9 - GraalVM: Python Quick Start
https://www.graalvm.org/python/quickstart/ - GraalVM Python: Interoperability
https://github.com/oracle/graalpython/blob/master/docs/user/Interoperability.md - Py4j na Stack Overflow
https://stackoverflow.com/tags/py4j/info - Py4J 0.8.2.1 Released
https://py4j.wordpress.com/ - PySpark
https://databricks.com/glossary/pyspark - PySpark Internals (Outdated)
https://cwiki.apache.org/confluence/display/SPARK/PySpark+Internals - JPype
http://jpype.sourceforge.net/index.html - JPype1 1.2.1 na PyPi
https://pypi.org/project/JPype1/ - 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/ - 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/