Ve druhém článku o zajímavém a potenciálně užitečném projektu Jython si ukážeme, jakým způsobem je možné v Jythonu používat rozhraní a třídy naimportované ze standardní knihovny jazyka Java. Uvidíme, že – což možná zní poněkud paradoxně – může být použití javovských knihoven snazší a jednodušší v Jythonu než v samotném programovacím jazyku Java.

Obsah

1. Použití standardní knihovny Javy v Jythonu

2. Základní způsoby využití tříd a rozhraní naprogramovaných v Javě z Jythonu

3. Zjednodušené volání getterů a setterů

4. Gettery vracející pravdivostní hodnoty true/false

5. Zkrácený zápis setterů a getterů – praktičtější příklady

6. Komplikace v případě, že jméno getteru/setteru koliduje se jménem atributu

7. Další série benchmarků pro porovnání rychlosti Jythonu, Pythonu 2.x a Pythonu 3.x

8. Výsledky benchmarku z předchozího článku

9. Třetí benchmark – intenzivní konkatenace (spojování) řetězců

10. Skripty pro spuštění benchmarku se zvoleným interpretrem a parametry

11. Výsledky běhu třetího benchmarku

12. „Skryté“ vytváření objektů v benchmarku s řetězci

13. Čtvrtý benchmark – Eratosthenovo síto

14. Skripty pro spuštění benchmarku se zvoleným interpretrem a parametry

15. Výsledky běhu čtvrtého benchmarku

16. Pád benchmarku v případě Jythonu

17. Jednoduchá GUI aplikace naprogramovaná v Jythonu

18. Nepatrně složitější příklad – dvě tlačítka na GUI

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

20. Odkazy na Internetu

1. Použití standardní knihovny Javy v Jythonu

V prvním článku o Jythonu jsme se seznámili s programovacími jazyky vytvořenými pro běh (spouštění) v nich vytvořených aplikací nad virtuálním strojem jazyka Javy (JVM – Java Virtual Machine). Připomeňme si, že se jedná například o jazyky Scala, Clojure, Groovy, Ruby (JRuby), Python (Jython) a v neposlední řadě taktéž JavaScript (implementované ve dvou projektech Rhino a Nashorn) i relativně nový jazyk Kotlin. Taktéž jsme si ukázali postup při instalaci Jythonu, spuštění interaktivní smyčky REPL, popsali jsme si základní knihovny dodávané společně s Jythonem a na závěr byly uvedeny dva benchmarky měřící rychlost provádění výpočtů s datovými typy float a complex.

Jazyky pro JVM zmíněné minule:

Jazyk pro JVM Stručný popis Odkaz
Java primární jazyk pro JVM, bajtkód odpovídá právě Javě https://www.oracle.com/java/index.html
Clojure moderní dialekt programovacího jazyka Lisp https://clojure.org/
Groovy dynamicky typovaný jazyk pro JVM http://groovy-lang.org/
Rhino jedna z implementací JavaScriptu https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Rhino
Nashorn alternativní implementace JavaScriptu https://blogs.oracle.com/nashorn/
JRuby portace jazyka Ruby na JVM http://jruby.org/
Jython portace jazyka Python na JVM http://www.jython.org/
Kotlin moderní staticky typovaný jazyk http://kotlinlang.org/
Scala další moderní jazyk pro JVM https://www.scala-lang.org/

Obrázek 1: Logo programovacího jazyka Jython, jehož popisu je dnešní článek věnován.

Dnes si řekneme, jakým způsobem je možné ve skriptech psaných v Jythonu používat rozhraní (interface) a třídy (class) nabízené základní knihovnou programovacího jazyka Java, která je dnes již velmi rozsáhlá (odkaz vede na knihovnu určenou pro JDK verze 1.9, ovšem podobně rozsáhlá byla již pro verze 1.5-1.8). Zajímavý a na první pohled možná poněkud paradoxní je fakt, že použití standardní knihovny Javy (a vlastně i mnoha dalších javovských knihoven) je v případě volání těchto knihoven z Jythonu mnohem jednodušší, než je tomu ve vlastním jazyku Java. To ostatně uvidíme i na dále popsaných demonstračních příkladech, jejichž verze psané v Jythonu jsou v naprosté většině případů kratší a přehlednější, než varianty vyvinuté přímo v programovacím jazyku Java. Jython je v tomto ohledu porovnatelný především s programovacím jazykem Groovy a v obecnějším pohledu i se Scalou a Kotlinem (i když typový systém těchto jazyků je odlišný).

2. Základní způsoby využití tříd a rozhraní naprogramovaných v Javě z Jythonu

Již v předchozím článku jsme se seznámili se základními způsoby komunikace mezi skripty psanými v programovacím jazyku Jython a třídami popř. rozhraními vytvořenými v Javě (včetně již zmíněné a rozsáhlé standardní knihovny Javy). Připomeňme si ve stručnosti, jak vlastně vypadá konstrukce objektu daného typu (třídy). V programovacím jazyku Java vypadá programový kód pro vytvoření nového objektu přibližně takto. Uvádíme si příklad na všeobecně známé implementaci seznamů představované třídou ArrayList implementující rozhraní List:


List l = new ArrayList();

Naproti tomu v případě Jythonu je situace poněkud jednodušší a především přehlednější, a to díky jeho dynamickému typovému systému. V praxi to znamená, že se nikde neuvádí typ proměnné, do které se přiřazuje právě vytvořená instance třídy. Navíc není nutné explicitně zapisovat operátor new, protože volání konstruktoru vypadá v Pythonu odlišně – zapisuje se podobně jako volání funkce, ovšem namísto jména funkce se použije jméno třídy (popř. rozhraní):


l = ArrayList()

Poznámka: třídu ArrayList je samozřejmě nutné nejdříve do skriptu naimportovat, což se provádí příkazem import třída nebo jeho variantou from balíček import třída:


from java.util import ArrayList

Typ objektu, resp. přesněji řečeno typ hodnoty, je uložen společně s objektem/hodnotou referencovanou v proměnné l. Můžeme se o tom snadno přesvědčit (například přímo v interaktivní smyčce Jythonu):

$ type(l)
 
<type 'java.util.ArrayList'>

Ve chvíli, kdy je instance třídy (tj. objekt) vytvořen, můžeme volat jeho metody popř. přistupovat k jeho atributům. Opět si to ukažme na velmi jednoduchém příkladu, v němž budeme volat dvě metody objektu typu ArrayList:

>>> l.append(1)
>>> l.size()
1

3. Zjednodušené volání getterů a setterů

Možnosti programovacího jazyka Jython jdou ovšem ještě dále, než je pouhá konstrukce objektů, volání jejich metod a přístup k atributům objektů. Pokud totiž třída obsahuje gettery a settery, tj. metody určené pro zjištění stavu popř. pro změnu stavu objektu, je možné gettery a settery volat nepřímo – přístupem (čtením či zápisem) do „kvaziatributu“, jehož jméno je odvozeno ze jména příslušného getteru a setteru. Ukažme si to opět na příkladu. Mějme třídu nazvanou CLS s getterem pojmenovaným getValue a setterem pojmenovaným setValue (pojmenování musíme zachovat, protože v Javě se gettery a settery identifikují právě svým jménem). Zdrojový kód této třídy naleznete na adrese https://github.com/tisnik/jython-examples/blob/master/CLS.java:


public class CLS {
    int v;

    public void setValue(int value) {
        this.v = value;
    }

    public int getValue() {
        return this.v;
    }
}

Ve chvíli, kdy si v Jythonu vytvoříme instanci této třídy, například do proměnné c, můžeme samozřejmě přímo volat getter a setter, a to prakticky stejně, jako v samotné Javě. Vše si ověříme v interaktivní smyčce REPL:

$ java -jar jython-standalone-2.7.0.jar 
Jython 2.7.0 (default:9987c746f838, Apr 29 2015, 02:25:11) 
[OpenJDK 64-Bit Server VM (Oracle Corporation)] on java1.7.0_79
Type "help", "copyright", "credits" or "license" for more information.
 
>>> import CLS
>>> c = CLS()
>>> c.setValue(42)
>>> c.getValue()
42

Popř. můžeme – což již v Javě nelze – použít zkráceného zápisu, v němž se namísto explicitního volání getterů a setterů čte či zapisuje kvaziatribut, jehož jméno je odvozeno od názvů getterů a setterů. Opět si ukažme příklad:

>>> c.value
42
>>> c.value=6502
>>> c.value
6502

Podobně je možné při práci s instancí třídy File použít setter nazvaný setExecutable(), který nastavuje bit „executable“:

>>> f2 = File("test")
 
>>> f2.createNewFile()
True
 
>>> f2.setExecutable(True)
True

Namísto setteru ovšem můžeme měnit hodnotu pseudoatributu executable, což je kratší a čitelnější:

>>> f3 = File("test2")
 
>>> f3.createNewFile()
True
 
>>> f3.executable=True
True

Výsledek si můžete ověřit přímo v souborovém systému.

4. Gettery vracející pravdivostní hodnoty true/false

Některé gettery, které vracejí pravdivostní hodnoty typu Boolean (objektový typ) nebo boolean (primitivní typ), většinou nebývají pojmenovány stylem getJménoAtributu ale spíše isJménoAtributu. V programovacím jazyku Jython však i s těmito gettery můžeme pracovat pořád stejně, tj. můžeme je buď volat explicitně jako jakoukoli jinou metodu nebo přečíst jejich hodnotu s využitím operátoru přiřazení.

Opět si tuto vlastnost ukážeme na jednoduchém demonstračním příkladu. Vytvoříme si instanci třídy File:

>>> from java.io import File
 
>>> f1=File(".")

Explicitní přístup ke getteru zjišťujícího informace o tom, zda instance třídy File reprezentuje soubor popř. zda reprezentuje adresář, vypadá naprosto stejně jako v Javě:

>>> f1.isFile()
False
 
>>> f1.isDirectory()
True

Alternativně ovšem můžeme ke stejným getterům přistupovat tak, jakoby se jednalo o běžné (čitelné) atributy:

>>> f1.file
False
 
>>> f1.directory
True

Ve skutečnosti se samozřejmě o běžné atributy nejedná, o čemž se můžeme snadno přesvědčit pokusem o zápis do nich:

>>> f1.file=False
 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: read-only attr: file

5. Zkrácený zápis setterů a getterů – praktičtější příklady

Samozřejmě nezapomeneme ani na praktičtější příklady, které souvisí s použitím setterů a getterů. Nejprve si vyzkoušíme použití getterů deklarovaných ve třídě java.awt.Color, která nese informace o barvě reprezentované v barvovém prostoru RGB. Nejprve vytvoříme instanci této třídy:


from java.awt import Color

c = Color(1, 0, 0)

A dále můžeme volat gettery objektu:


print(c.getRed())
print(c.getGreen())
print(c.getBlue())
print(c.getAlpha())

Popř. použít zkrácený zápis:


print(c.red)
print(c.green)
print(c.blue)
print(c.alpha)

Podívejme se na celý výpis tohoto demonstračního příkladu:


from java.awt import Color

print("red")

c = Color(1, 0, 0)

print(c.getRed())
print(c.getGreen())
print(c.getBlue())
print(c.getAlpha())

print("")

print(c.red)
print(c.green)
print(c.blue)
print(c.alpha)


print("\n\nyellow")

c2 = Color.YELLOW

print(c2.red)
print(c2.green)
print(c2.blue)
print(c2.alpha)

Po spuštění by se na standardní výstup měly vypsat tyto údaje:

red
1
0
0
255

1
0
0
255


yellow
255
255
0
255

Druhý příklad je kratší a ukazuje dvě možnosti, jak napsat test, jestli je nějaká kolekce prázdná či nikoli:

from java.util import ArrayList

a = ArrayList()

print(a.isEmpty())
print(a.empty)

6. Komplikace v případě, že jméno getteru/setteru koliduje se jménem atributu

Ovšem ne vždy je situace se settery a ještě více s gettery tak snadná jako tomu bylo v předchozích demonstračních příkladech. Může totiž dojít k situaci, kdy nějaká třída obsahuje getter/setter a současně i viditelný atribut (nebo metodu!) stejného jména. V takovém případě dojde k problémům (kvůli nejednoznačnosti), které si můžeme ukázat na dalším demonstračním příkladu:


from java.lang import StringBuffer

s = StringBuffer("Hello world!")

print(s.length())
print(s)
print("")

s.setLength(11)

print(s.length())
print(s)
print("")

s.length = 5

print(s.length())
print(s)
print("")

Výsledek po spuštění:

$ java -jar jython-standalone-2.7.0.jar set_length.py

12
Hello world!

11
Hello world

Traceback (most recent call last):
  File "set_length.py", line 15, in <module>
    s.length = 5
TypeError: readonly attribute

Proč tomu tak je? Problém spočívá v tom, že třída StringBuffer obsahuje metody setLength() a length(), což do jisté míry porušuje konvence java beans. Proto je přiřazení s.length = 5 nejednoznačné a je nutné použít explicitní volání setteru.

7. Další série benchmarků pro porovnání rychlosti Jythonu, Pythonu 2.x a Pythonu 3.x

Ve druhé části dnešního článku si opět ukážeme několik benchmarků, protože téma efektivity či (nutno přiznat) většinou spíše neefektivity Jythonu v porovnání s dalšími programovacími jazyky, resp. přesněji řečeno s dalšími interpretry Pythonu, se neustále diskutuje a rychlost popř. pomalost Jythonu může vést k tomu, že se tento interpret nebude moci pro konkrétní typ aplikace použít (pomalost Jythonu je jen jednou z nevýhod tohoto jazyka; další nevýhodou je, že Jython je založený na Pythonu 2 a nikoli Pythonu 3). Je tedy vhodné vědět již před zahájením práce na nové aplikaci, jestli bude Jython přínosem či zda naopak bude aplikace nepřiměřeně pomalá nebo náročná na systémové prostředky. Nejprve si připomeneme, k jakým výsledkům jsme došli minule a posléze si ukážeme dva benchmarky zaměřené na práci s řetězci a na použití generátorů a zpracování seznamů.

8. Výsledky benchmarku z předchozího článku

Jen v rychlosti si připomeňme, že minule jsme si ukázali dva benchmarky, které byly zaměřeny především na rychlost výpočtů s hodnotami typu float a complex. Výsledky prvního benchmarku (výpočtu Mandelbrotovy množiny s uložením výsledného rastrového obrázku do souboru typu PPM) vypadají následovně. Měření samozřejmě probíhalo na totožném počítači:

# x-res y-res Jython (s) Python 2 (s) Python 3 (s)
1 16 16 1.79 0.01 0.02
2 24 24 1.79 0.01 0.02
3 32 32 1.84 0.02 0.02
4 48 48 2.11 0.03 0.03
5 64 64 2.01 0.04 0.05
6 96 96 2.16 0.08 0.09
7 128 128 2.24 0.15 0.15
8 192 192 2.43 0.32 0.33
9 256 256 2.81 0.57 0.58
10 384 384 3.87 1.25 1.29
11 512 512 5.05 2.27 2.28
12 768 768 8.61 5.07 5.21
13 1024 1024 13.22 9.00 9.10
14 1536 1536 28.15 20.73 21.24
15 2048 2048 50.03 36.11 38.24
16 3072 3072 100.78 81.93 84.02
17 4096 4096 179.21 144.45 148.44

Obrázek 1: Výsledky prvního benchmarku prezentovaného v předchozím článku (výpočet Mandelbrotovy množiny) vynesené do grafu.

Z výsledků druhého benchmarku, který taktéž provádí výpočet Mandelbrotovy množiny, ovšem s datovým typem complex (ten v Javě jako primitivní datový typ neexistuje), je patrné, že je Jython při práci s komplexními čísly výrazně pomalejší než nativní CPython 2.x i CPython 3.x:

# x-res y-res Jython (s) Python 2 (s) Python 3 (s)
1 16 16 1.77 0.01 0.02
2 24 24 1.99 0.01 0.02
3 32 32 1.80 0.02 0.03
4 48 48 1.90 0.03 0.04
5 64 64 1.99 0.04 0.06
6 96 96 2.20 0.08 0.11
7 128 128 2.70 0.14 0.18
8 192 192 3.15 0.32 0.43
9 256 256 4.13 0.56 0.77
10 384 384 6.61 1.25 1.60
11 512 512 10.10 2.22 2.71
12 768 768 20.59 5.12 6.37
13 1024 1024 34.45 9.09 10.78
14 1536 1536 77.73 20.33 24.25
15 2048 2048 134.13 35.95 43.61
16 3072 3072 294.04 81.64 99.77
17 4096 4096 523.57 148.13 176.97

Obrázek 2: Výsledky druhého benchmarku (výpočet Mandelbrotovy množiny s použitím datového typu complex) vynesené do grafu.

První poučení tedy zní – pokud použijeme Jython, je vhodné si dát pozor na to, jak jsou zpracovávány hodnoty typu complex a zda se nebude jednat o úzké hrdlo programu.

9. Třetí benchmark – intenzivní konkatenace (spojování) řetězců

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)

Pro větší hodnoty n bude s velkou pravděpodobností hrát větší roli rychlost konkatenace řetězců, takže si počkejme na výsledky, zda toto očekávání potvrdí.

Následuje výpis celého zdrojového kódu tohoto mikrobenchmarku. Testovanou funkcí bude perform_benchmark():


#!/usr/bin/env python
# vim: set fileencoding=utf-8

from sys import argv, exit


def perform_benchmark(repeat_count):
    s = ""
    for i in range(1, 1 + repeat_count):
        s += str(i) + " "

    print(len(s))
    return s


if __name__ == "__main__":
    if len(argv) < 2:
        print("usage: python string_concat repeat_count")
        exit(1)

    repeat_count = int(argv[1])
    perform_benchmark(repeat_count)

10. Skripty pro spuštění benchmarku se zvoleným interpretrem a parametry

Pro spuštění benchmarku použijeme trojici prakticky shodných skriptů, které budou postupně zvětšovat počet čísel (resp. řetězcové podoby těchto čísel) připojovaných k řetězci. Oproti benchmarkům uvedeným minule se vždy počet zvýší na dvojnásobek předchozí hodnoty. Pro malý počet iterací se tedy bude spíše měřit rychlost nastartování interpretru Pythonu (verze 2 či 3) popř. virtuálního stroje Javy a inicializace Jythonu, ovšem u vyšších hodnot již začne převládat rychlost manipulace s řetězci, vliv činnosti správce paměti apod.

Skript pro Jython

Tento skript vyžaduje, aby se v aktuálním adresáři nacházel Java archiv s Jythonem popř. jen symbolický link na tento archiv (jython-standalone-2.7.0.jar):


repeat_count=1
limit=10000000

OUTFILE="jython.times"
PREFIX="jython"

rm -f $OUTFILE
rm -f ${PREFIX}.txt

while [ $repeat_count -lt $limit ]
do
    echo $repeat_count
    echo -n "$repeat_count " >> $OUTFILE
    /usr/bin/time --output $OUTFILE --append --format "%e %M" java -jar jython-standalone-2.7.0.jar string_concat.py $repeat_count >> "${PREFIX}.txt"
    repeat_count=$(( $repeat_count * 2 ))
done

Povšimněte si, jakým způsobem se postupně zvyšuje počet iterací.

Skript pro Python 2.x

Skript určený pro klasický interpret Pythonu 2 vypadá následovně:


repeat_count=1
limit=10000000

OUTFILE="python2.times"
PREFIX="python2"

rm -f $OUTFILE
rm -f ${PREFIX}.txt

while [ $repeat_count -lt $limit ]
do
    echo $repeat_count
    echo -n "$repeat_count " >> $OUTFILE
    /usr/bin/time --output $OUTFILE --append --format "%e %M" python2 -B string_concat.py $repeat_count >> "${PREFIX}.txt"
    repeat_count=$(( $repeat_count * 2 ))
done

Skript pro Python 3.x

Skript pro interpret Pythonu 3 se prakticky neliší od předchozího skriptu, což je ovšem pochopitelné:


repeat_count=1
limit=10000000

OUTFILE="python3.times"
PREFIX="python3"

rm -f $OUTFILE
rm -f ${PREFIX}.txt

while [ $repeat_count -lt $limit ]
do
    echo $repeat_count
    echo -n "$repeat_count " >> $OUTFILE
    /usr/bin/time --output $OUTFILE --append --format "%e %M" python3 -B string_concat.py $repeat_count >> "${PREFIX}.txt"
    repeat_count=$(( $repeat_count * 2 ))
done

11. Výsledky běhu třetího benchmarku

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 a taktéž vypsány v následující tabulce.

Obrázek 3: 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é.

Zde prakticky není důvod pro další zkoumání naměřených hodnot, protože Jython je zde evidentně mnohem pomalejší.

Obrázek 4: Výsledky třetího benchmarku bez několika posledních iterací.

Všechny časy jsou uvedeny v sekundách a pokud je namísto času zapsán znak ×, znamená to pád benchmarku vysvětlený níže:

# iter Jython (s) Python 2 (s) Python 3 (s)
1 1 1.81 0.01 0.02
2 2 1.79 0.01 0.02
3 4 1.79 0.01 0.02
4 8 1.76 0.01 0.02
5 16 1.74 0.01 0.02
6 32 1.74 0.01 0.02
7 64 1.74 0.01 0.02
8 128 1.81 0.01 0.02
9 256 1.83 0.01 0.02
10 512 1.77 0.01 0.02
11 1024 1.85 0.01 0.02
12 2048 1.78 0.01 0.02
13 4096 1.76 0.01 0.02
14 8192 1.99 0.01 0.02
15 16384 2.11 0.01 0.02
16 32768 2.83 0.01 0.03
17 65536 5.81 0.02 0.04
18 131072 16.96 0.04 0.05
19 262144 68.53 0.06 0.09
20 524288 326.14 0.12 0.16
21 1048576 1546.85 0.23 0.31
22 2097152 4644.08 0.44 0.62
23 4194304 × 0.86 1.23
24 8388608 × 1.74 2.45

V tabulce můžeme vidět mnoho zajímavých údajů a trendů. Zejména z porovnání Pythonu 2.x a 3.x vyplývá, že Python 2.x je při práci s řetězci nepatrně rychlejší. Je tomu tak z toho důvodu, že v Pythonu 3.3 a v dalších verzích se s řetězci pracuje odlišně, což je téma, kterému jsem se věnoval na konkurenčním serveru. Zcela odlišné jsou výsledky v případě Jythonu, kde nejprve měříme především rychlost startu JVM, od přibližně 10000 iterací se čas postupně zvyšuje a od určité hranice (cca 200000 iterací) můžeme vidět, že rychlost zpracování klesá a pro největší počet iterací navíc došlo k pádu aplikace kvůli překročení nastaveného limitu alokované paměti pro JVM (!):

Traceback (most recent call last):
  File "string_concat.py", line 22, in <module>
    perform_benchmark(repeat_count)
  File "string_concat.py", line 9, in perform_benchmark
    for i in range(1, 1 + repeat_count):
java.lang.OutOfMemoryError: Java heap space
        at org.python.core.Py.newInteger(Py.java:596)
        at org.python.core.PyInteger.int___add__(PyInteger.java:312)
        at org.python.core.PyInteger.__add__(PyInteger.java:299)
        at org.python.core.PyObject._basic_add(PyObject.java:2133)
        at org.python.core.PyObject._add(PyObject.java:2119)
        at org.python.core.__builtin__.range(__builtin__.java:928)
        at org.python.core.__builtin__.range(__builtin__.java:903)
        at org.python.core.BuiltinFunctions.__call__(__builtin__.java:125)
        at org.python.core.PyObject.__call__(PyObject.java:482)
        at org.python.pycode._pyx0.perform_benchmark$1(string_concat.py:13)
        at org.python.pycode._pyx0.call_function(string_concat.py)
        at org.python.core.PyTableCode.call(PyTableCode.java:167)
        at org.python.core.PyBaseCode.call(PyBaseCode.java:138)
        at org.python.core.PyFunction.__call__(PyFunction.java:413)
        at org.python.pycode._pyx0.f$0(string_concat.py:22)
        at org.python.pycode._pyx0.call_function(string_concat.py)
        at org.python.core.PyTableCode.call(PyTableCode.java:167)
        at org.python.core.PyCode.call(PyCode.java:18)
        at org.python.core.Py.runCode(Py.java:1386)
        at org.python.util.PythonInterpreter.execfile(PythonInterpreter.java:296)
        at org.python.util.jython.run(jython.java:362)
        at org.python.util.jython.main(jython.java:142)

java.lang.OutOfMemoryError: java.lang.OutOfMemoryError: Java heap space

12. „Skryté“ vytváření objektů v benchmarku s řetězci

Můžeme se ptát, v čem vlastně spočívá pomalost Jythonu při opakovaných konkatenacích řetězců? Na prvním místě si musíme ozřejmit uvědomit, jakým způsobem s řetězci pracuje virtuální stroj Javy, tj. JVM. Bajtkód příslušné funkce po jejím překladu do bajtkódu vypadá přibližně takto:

   0:   ldc             #2; //String
   2:   astore_0
   3:   iconst_0
   4:   istore_1
   5:   iload_1
   6:   sipush  10000
   9:   if_icmpge 42        // na bajtu s indexem 42 je konec smyčky
*  12:  new             #3; //class java/lang/StringBuilder
*  15:  dup
*  16:  invokespecial   #4; //Method java/lang/StringBuilder."(init)":()V
*  19:  aload_0
*  20:  invokevirtual   #5; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
*  23:  iload_1
*  24:  invokevirtual   #6; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
*  27:  ldc             #7; //String
*  29:  invokevirtual   #5; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
*  32:  invokevirtual   #8; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
*  35:  astore_0
   36:  iinc    1, 1        // zvýšení hodnoty počitadla
   39:  goto    5           // skok zpět na začátek smyčky
   42:  aload_0
   43:  areturn

Vzhledem k tomu, že výše uvedený bajtkód je pro velkou část programátorů v Javě či Pythonu poměrně nepřehledný a špatně čitelný, je níže pro ilustraci vypsán programový kód, který je do co největší míry ekvivalentní s instrukcemi, jež se vyskytují ve vygenerovaném bajtkódu. Povšimněte si, že se v tomto zdrojovém kódu instance třídy StringBuilder ihned po svém vytvoření (konstrukci) naplní původním řetězcem, ke kterému se mají připisovat další znaky. Posléze se do tohoto objektu přidá textová reprezentace celého čísla následovaná řetězcem obsahujícím mezeru. Poslední operací, která se s instancí třídy StringBuffer provádí, je převod jejího atributu zpět na řetězec:


String str = "";
for (int i = 0; i < LOOP_COUNT; i++)
{
    StringBuilder tmp = new StringBuilder();
    tmp.append(str);
    tmp.append(i);
    tmp.append(" ");
    str = tmp.toString();
}
return str;

A právě neustálé vytváření objektů uvnitř programové smyčky a opakované připojování již vytvořeného řetězce do StringBuilderu je velmi náročné jak na paměť, tak i na výkonnost automatického správce paměti. V interpretrech Pythonu je tato činnost evidentně vyřešena mnohem efektivněji

13. Čtvrtý benchmark – Eratosthenovo síto

Č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. Zdrojový kód benchmarku vypadá následovně:


#!/usr/bin/env python
# vim: set fileencoding=utf-8

from sys import argv


def sieve(n):
    multiples = set()
    for i in range(2, n+1):
        if i not in multiples:
            yield i
            multiples.update(range(i*i, n+1, i))


if __name__ == "__main__":
    if len(argv) < 2:
        print("usage: python sieve_algorithm repeat_count")
        exit(1)

    max_value = int(argv[1])
    primes = list(sieve(max_value))
    print(len(primes))

14. Skripty pro spuštění benchmarku se zvoleným interpretrem a parametry

Pro spuštění posledního benchmarku opět použijeme trojici prakticky shodných skriptů, které budou postupně zvětšovat počet generovaných prvočísel. Oproti benchmarkům uvedeným minule se vždy počet zvýší na dvojnásobek předchozí hodnoty. Pro malý počet iterací se tedy bude spíše měřit rychlost nastartování interpretru Pythonu (verze 2 či 3) popř. virtuálního stroje Javy a inicializace Jythonu, ovšem u vyšších hodnot již začne převládat rychlost nebo pomalost vlastního algoritmu.

Skript pro Jython


max_value=1
limit=100000000

OUTFILE="jython.times"
PREFIX="jython"

rm -f $OUTFILE
rm -f ${PREFIX}.txt

while [ $max_value -lt $limit ]
do
    echo $max_value
    echo -n "$max_value " >> $OUTFILE
    /usr/bin/time --output $OUTFILE --append --format "%e %M" java -jar jython-standalone-2.7.0.jar sieve_algorithm.py $max_value >> "${PREFIX}.txt"
    max_value=$(( $max_value * 2 ))
done

Skript pro Python 2.x


max_value=1
limit=100000000

OUTFILE="python2.times"
PREFIX="python2"

rm -f $OUTFILE
rm -f ${PREFIX}.txt

while [ $max_value -lt $limit ]
do
    echo $max_value
    echo -n "$max_value " >> $OUTFILE
    /usr/bin/time --output $OUTFILE --append --format "%e %M" python2 -B sieve_algorithm.py $max_value >> "${PREFIX}.txt"
    max_value=$(( $max_value * 2 ))
done

Skript pro Python 3.x


max_value=1
limit=100000000

OUTFILE="python3.times"
PREFIX="python3"

rm -f $OUTFILE
rm -f ${PREFIX}.txt

while [ $max_value -lt $limit ]
do
    echo $max_value
    echo -n "$max_value " >> $OUTFILE
    /usr/bin/time --output $OUTFILE --append --format "%e %M" python3 -B sieve_algorithm.py $max_value >> "${PREFIX}.txt"
    max_value=$(( $max_value * 2 ))
done

15. Výsledky běhu čtvrtého benchmarku

Podívejme se nyní na výsledky benchmarku, nejprve v grafové podobě. Z výsledků je patrné, že benchmark spuštěný v Jythonu je opět nejpomalejší a navíc se se zvyšujícím se nejvyšším prvočíslem dále zpomaluje (minimálně v porovnání s implementací Pythonu 2 i 3). Zajímavé je, že až na pomalejší start je Python 3 nepatrně rychlejší, než Python 2:

Obrázek 4: Výsledky čtvrtého benchmarku (konkatenace řetězců) vynesené do grafu.

Stejné výsledky, tentokrát zapsané do tabulky:

# iter Jython (s) Python 2 (s) Python 3 (s)
1 1 2.70 0.01 0.02
2 2 2.71 0.01 0.02
3 4 2.60 0.01 0.02
4 8 2.58 0.01 0.02
5 16 2.53 0.01 0.02
6 32 2.04 0.01 0.02
7 64 1.89 0.01 0.02
8 128 1.83 0.01 0.02
9 256 1.93 0.01 0.02
10 512 2.04 0.01 0.02
11 1024 1.92 0.01 0.02
12 2048 1.82 0.01 0.02
13 4096 1.80 0.01 0.02
14 8192 1.87 0.01 0.02
15 16384 1.83 0.01 0.02
16 32768 1.85 0.01 0.03
17 65536 1.82 0.02 0.03
18 131072 1.93 0.04 0.05
19 262144 2.04 0.08 0.08
20 524288 2.29 0.18 0.17
21 1048576 2.99 0.41 0.35
22 2097152 3.95 0.88 0.73
23 4194304 5.64 1.86 1.44
24 8388608 12.99 3.96 2.88
25 16777216 26.61 8.20 5.95
26 33554432 123.79 16.80 12.29
27 67108864 273.94 34.88 25.75

16. Pád benchmarku v případě Jythonu

Jen na okraj – podobně, jako se to stalo v případě benchmarku pro konkatenaci řetězců, můžeme podobným způsobem „narazit“ i u čtvrtého benchmarku, v němž poměrně brzy dojde k pádu aplikace kvůli problémům s alokací paměti. Opět to souvisí s odlišným způsobem interní reprezentace objektů v JVM a v interpretrech Pythonu (konkrétně CPythonu):

Traceback (most recent call last):
  File "sieve_algorithm.py", line 21, in 
    primes = list(sieve(max_value))
  File "sieve_algorithm.py", line 12, in sieve
    multiples.update(range(i*i, n+1, i))
java.lang.OutOfMemoryError: GC overhead limit exceeded
        at java.util.concurrent.ConcurrentHashMap.putVal(ConcurrentHashMap.java:1019)
        at java.util.concurrent.ConcurrentHashMap.put(ConcurrentHashMap.java:1006)
        at java.util.Collections$SetFromMap.add(Collections.java:5461)
        at org.python.core.BaseSet._update(BaseSet.java:47)
        at org.python.core.BaseSet._update(BaseSet.java:28)
        at org.python.core.PySet.set_update(PySet.java:297)
        at org.python.core.PySet$set_update_exposer.__call__(Unknown Source)
        at org.python.core.PyObject.__call__(PyObject.java:461)
        at org.python.core.PyObject.__call__(PyObject.java:465)
        at org.python.pycode._pyx0.sieve$1(sieve_algorithm.py:9)
        at org.python.pycode._pyx0.call_function(sieve_algorithm.py)
        at org.python.core.PyTableCode.call(PyTableCode.java:167)
        at org.python.core.PyGenerator.__iternext__(PyGenerator.java:156)
        at org.python.core.PyGenerator.__iternext__(PyGenerator.java:138)
        at org.python.core.WrappedIterIterator.hasNext(WrappedIterIterator.java:23)
        at org.python.core.PyList.list___init__(PyList.java:138)
        at org.python.core.PyList$exposed___new__.createOfType(Unknown Source)
        at org.python.core.PyOverridableNew.new_impl(PyOverridableNew.java:12)
        at org.python.core.PyType.invokeNew(PyType.java:494)
        at org.python.core.PyType.type___call__(PyType.java:1706)
        at org.python.core.PyType.__call__(PyType.java:1696)
        at org.python.core.PyObject.__call__(PyObject.java:461)
        at org.python.core.PyObject.__call__(PyObject.java:465)
        at org.python.pycode._pyx0.f$0(sieve_algorithm.py:22)
        at org.python.pycode._pyx0.call_function(sieve_algorithm.py)
        at org.python.core.PyTableCode.call(PyTableCode.java:167)
        at org.python.core.PyCode.call(PyCode.java:18)
        at org.python.core.Py.runCode(Py.java:1386)
        at org.python.util.PythonInterpreter.execfile(PythonInterpreter.java:296)
        at org.python.util.jython.run(jython.java:362)
        at org.python.util.jython.main(jython.java:142)

java.lang.OutOfMemoryError: java.lang.OutOfMemoryError: GC overhead limit exceeded

17. Jednoduchá GUI aplikace naprogramovaná v Jythonu

V samotném závěru dnešního článku si ukážeme, jak jednoduché je v Jythonu vytvořit aplikaci s grafickým uživatelským rozhraním založeným na knihovně Swing. Oproti kódu naprogramovanému v Javě je skript napsaný v Jythonu velmi krátký, což do značné míry souvisí s dynamickým typovým systémem a taktéž s tím, že v Jythonu jsou funkce plnohodnotným datovým typem, na rozdíl od Javy. To mj. znamená, že je možné běžnou funkci použít jako handler nějaké události, což by se v Javě muselo řešit s využitím anonymní třídy:


button = JButton('Test', actionPerformed=on_button_clicked)

from javax.swing import JButton
from javax.swing import JFrame


frame = JFrame("Simple GUI",
               defaultCloseOperation=JFrame.EXIT_ON_CLOSE,
               size=(320, 240))


def on_button_clicked(event):
    print 'Button clicked!'


button = JButton('Test', actionPerformed=on_button_clicked)
frame.add(button)
frame.visible = True

18. Nepatrně složitější příklad – dvě tlačítka na GUI

Ve druhém podobně laděném příkladu je ukázáno, že handlerem události může být i anonymní funkce, která se v Pythonu vytváří s využitím klíčového slova lambda:


import sys
from javax.swing import JButton
from javax.swing import JFrame
from java.awt import Component, GridLayout

frame = JFrame("Simple GUI",
               defaultCloseOperation=JFrame.EXIT_ON_CLOSE,
               size=(320, 240))

contentPane = frame.getContentPane()
contentPane.setLayout(GridLayout(2, 1))


def on_button_clicked(event):
    print 'Button clicked!'


button1 = JButton('Test', actionPerformed=on_button_clicked)
button2 = JButton('Quit', actionPerformed=lambda event:sys.exit())

frame.add(button1)
frame.add(button2)
frame.visible = True

Složitější ukázky podobným způsobem vytvořených aplikací si ukážeme příště.

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

Všechny demonstrační příklady, které jsme si v dnešním článku ukázali, jsou uloženy v repositáři, který naleznete na adrese https://github.com/tisnik/jython-examples. Následují odkazy na jednotlivé příklady (pro jejich spuštění je nutné mít v aktuálním adresáři symbolický link na Java archiv jython-standalone-2.7.0.jar):

Zdrojový kód/skript Adresa
array_list_is_empty.py https://github.com/tisnik/jython-examples/blob/master/arraylist_is_empty/array_is_empty.py
button_class.py https://github.com/tisnik/jython-examples/blob/master/attribute_lookup/button_class.py
color_class.py https://github.com/tisnik/jython-examples/blob/master/attribute_lookup/color_class.py
string_buffer_class.py https://github.com/tisnik/jython-examples/blob/master/attribute_lookup/string_buffer_class.py
color_getters.py https://github.com/tisnik/jython-examples/blob/master/
string_buffer_length.py https://github.com/tisnik/jython-examples/blob/master/
simple_gui.py https://github.com/tisnik/jython-examples/blob/master/
two_buttons.py https://github.com/tisnik/jython-examples/blob/master/

Dnes popsané benchmarky (v pořadí třetí a čtvrtý) se skládají z většího množství souborů a proto jsou vypsány v samostatných tabulkách:

Třetí benchmark

Zdrojový kód/skript Adresa
string_concat.py https://github.com/tisnik/jython-examples/blob/master/benchmark_string_concat/mandelbrot_complex.py
test_jython.sh https://github.com/tisnik/jython-examples/blob/master/benchmark_string_concat/test_jython.sh
test_python2.sh https://github.com/tisnik/jython-examples/blob/master/benchmark_string_concat/test_python2.sh
test_python3.sh https://github.com/tisnik/jython-examples/blob/master/benchmark_string_concat/test_python3.sh
clean.sh https://github.com/tisnik/jython-examples/blob/master/benchmark_string_concat/clean.sh

Čtvrtý benchmark

Zdrojový kód/skript Adresa
sieve_algorithm.py https://github.com/tisnik/jython-examples/blob/master/benchmark_sieve_algorithm/mandelbrot_complex.py
test_jython.sh https://github.com/tisnik/jython-examples/blob/master/benchmark_sieve_algorithm/test_jython.sh
test_python2.sh https://github.com/tisnik/jython-examples/blob/master/benchmark_sieve_algorithm/test_python2.sh
test_python3.sh https://github.com/tisnik/jython-examples/blob/master/benchmark_sieve_algorithm/test_python3.sh
clean.sh https://github.com/tisnik/jython-examples/blob/master/benchmark_sieve_algorithm/clean.sh

20. Odkazy na Internetu

  1. Stránka projektu Jython
    http://www.jython.org/
  2. Jython (Wikipedia)
    https://en.wikipedia.org/wiki/Jython
  3. Scripting for the Java Platform (Wikipedia)
    https://en.wikipedia.org/wiki/Scripting_for_the_Java_Platform
  4. JSR 223: Scripting for the JavaTM Platform
    https://jcp.org/en/jsr/detail?id=223
  5. List of JVM languages
    https://en.wikipedia.org/wiki/List_of_JVM_languages
  6. Stránka programovacího jazyka Java
    https://www.oracle.com/java/index.html
  7. Stránka programovacího jazyka Clojure
    http://clojure.org
  8. Stránka programovacího jazyka Groovy
    http://groovy-lang.org/
  9. Stránka programovacího jazyka JRuby
    http://jruby.org/
  10. Stránka programovacího jazyka Kotlin
    http://kotlinlang.org/
  11. Stránka programovacího jazyka Scala
    https://www.scala-lang.org/
  12. Projekt Rhino
    https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Rhino
  13. Clojure (Wikipedia)
    https://en.wikipedia.org/wiki/Clojure
  14. Groovy (Wikipedia)
    https://en.wikipedia.org/wiki/Groovy_%28programming_language%29
  15. JRuby (Wikipedia)
    https://en.wikipedia.org/wiki/JRuby
  16. Kotlin (Wikipedia)
    https://en.wikipedia.org/wiki/Kotlin_%28programming_language%29
  17. Scala (Wikipedia)
    https://en.wikipedia.org/wiki/Scala_%28programming_language%29
  18. Python Interpreters Benchmarks
    https://pybenchmarks.org/u64q/jython.php
  19. Apache Kafka Producer Benchmarks – Java vs. Jython vs. Python
    http://mrafayaleem.com/2016/03/31/apache-kafka-producer-benchmarks/
  20. What is Jython and is it useful at all? (Stack Overflow)
    https://stackoverflow.com/questions/1859865/what-is-jython-and-is-it-useful-at-all
  21. Sieve of Eratosthenes (Wikipedia)
    https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes
  22. Sieve of Eratosthenes (Geeks for Geeks)
    https://www.geeksforgeeks.org/sieve-of-eratosthenes/
  23. Sieve of Eratosthenovo (Rosetta code)
    http://rosettacode.org/wiki/Sieve_of_Eratosthenes
  24. Monitorování procesů a správa paměti v JDK6 a JDK7 (1)
    https://www.root.cz/clanky/monitorovani-procesu-a-sprava-pameti-v-jdk6-a-jdk7-1/
  25. Monitorování procesů a správa paměti v JDK6 a JDK7 (2)
    https://www.root.cz/clanky/monitorovani-procesu-a-sprava-pameti-v-jdk6-a-jdk7-2/