Jython 2.7:結合 Python 與 Java

可以輕易建立同時使用 Python 與 Java 函式庫專案的語言

Du Spirit
Java Magazine 翻譯系列
20 min readApr 27, 2024

--

Translated from “Jython 2.7: Integrating Python and Java” by Jim Baker and Josh Juneau, Java Magazine, November/December 2015, page 42. Copyright Oracle Corporation.

在廣大社群、強健的生態系與穩健的語言加持下,Python 的開發者長久以來享有極高的開發效率,Jython 是 Python 其中一個可在 JVM 上運行的實作,選擇 Jython 的理由多樣廣泛,您可能因為想在 Python 程式中使用 Java 套件而選擇它、透過 Python 的互動終端探究 Java 生態系、佈署使用 Django 的 Python 程式到 servlet 容器中、或是將您的 Java 專案與腳本語言 (一些受歡迎的工具如 Sikuli、The Grinder 和 IBM WebSphere) 一起打包。

第一個 Jython 釋出版本 2.0,支援 Python 2.0,在 2001 年首次見到曙光 (先前的版本稱為 “JPython”),Jython 從此成長,並成為 Java 平台上最成熟與穩定的替代語言之一,最近的版本 Jython 2.7,在 2015 五月釋出,支援 Python 2.7、加強與 Java 的整合、和擴充對 Python 生態系的支援,特別是 Python 套件管理。

本文提供 Jython 2.7 詳細的導覽,包含一個容易上手的例子:使用 Apache POI 操作試算表,讀完本文,您將對 Jython 特性有足夠的了解,可以下載最新的釋出版本,然後開始您自己的專案。

精簡入門書 — 它就只是 Python

Jython 就是 Java 平台上的 Python,但是,如果您不熟悉 Python 語言,你需要精簡入門書。

我們將用 Jython 終端來看語言特性,Jython 終端使用受歡迎的 JLine 2 實作經典的 read-evaluate-print loop (REPL),終端讀取使用者輸入的述句(statements) 和表示式 (expressions),對述句進行計算,並印出結果,這流程 (迴圈) 會持續到使用者離開終端為止。類似的終端,針對 JVM 其它受歡迎的語言像是 Clojure、Groovy、JRuby 和 Scala,同樣用 JLine 2 開發。

不帶任何參數執行 Jython 程式將會啟動終端,我們將會使用撰寫本文時最新釋出的版本 (2.7.0),如 Listing 1 所示 (注意,在本文中,我們假設您使用 UNIX-like 的系統,$ 是像 bash 的命令列提示。Jython 同樣能在 Windows上運作)。

Listing 1

終端提示您從 >>> 開始輸入,我們開始使用 dict 型別,即 dictionary [譯註:我想這裡翻成字典應該沒什麼幫助],一個 key 到 value 的對應 (可修改),key 和 value 可以是 Python 或 Java 的任何物件,馬上就會看到。因它的多功能,dictionary 在大多數的 Python 程式中大量被使用,Listing 2 用幾個例子說明 dictionary 用法。

Listing 2

Python 2.7 同樣支援 dictionary comprehension [譯註:comprehension 該翻成什麼?這真的考倒我了,就下面範例我知道數學上的意思,但暫時找不到合適的詞,網路上有推導或綜合運算等翻法,但都覺得不怎麼達意,所以,暫時不翻它了],comprehension 是用特定語法用一個算式產生特定型別容器的一種用法,比如,用下面的 comprehension,我們可以得到將原本 dictionary 中每個元素的 value 對到 key 的反向對應:

>>> inverted_d1 = { v: k for k, v in d1.iteritems() }

簡潔 comprehension 語法的變形同樣支援用來產生 list 和 set。

反向對應的功能常用到我們可能再用一次,因此,我們定義一個函式封裝它,當我們可以在終端裡定義 Python 函式,這對我們同樣是一個好機會探索終端的其他用法,建立一個 basics.py 檔案,並將下列文字作為該檔案初始的內容:

def inverted(d): return { v: k for k,v in d.iteritems() }

我們先詳細看一下程式碼片段。首先,inverted 函式使用 def 關鍵字定義,注意 Python 使用有意義的空白,代替大括號或其他符號來描述程式的階層結構 (例中的大括弧是用來建立 dictionary comprehension,大括弧用於建立 dictset,同樣,雖然一般是四個空白。因文章的限制,我們用二個空白縮排 Python 程式),Python 的哲學很簡單:我們已經縮排程式,所以它相應於它的結構,用大括弧或其他符號是多餘的。但許多格式化的細節,您也許需要一點時間適應 Python 的作法。

讓我們再次回到 Jython 終端,但此次,我們用 jython27 –i basics.py 開啟終端並載入我們的檔案,在命令提示 >>> 中,我們呼叫 dir 函式來查詢有什麼可用,當不帶參數呼叫這函式時,這只適用目前的模組:

$ jython27 -i basics.py
>>> dir()
['__builtins__', '__doc__',
'__file__',
'__name__', '__package__',
'inverted']
>>> inverted({1: 'one', 2: 'two'})
{'one': 1, 'two': 2}

-i 選項指我們在 basics 這模組的有效範圍中運行終端,我們有許多已定義的名字,包含我們剛定義的 inverted 函式,這樣使用 REPL 的方式非常適合探索式的程式開發:用正在進行開發的模組開啟終端、在終端中嘗試一個想法、然候用終端中可用的部分抽出來編輯模組,重複這過程。這探索式開發是使用 Python 開發的一項保證。

您可以用較傳統的方式開發程式,因此像 PyDev (建構在 Eclipse 之上) 或 PyCharm (建構在 IntelliJ 之上) 等IDE,提供 Python 程式的 GUI 除錯工具,包含中斷點、監看、檢視變數等,這些都是可行的,因為 Jython 支援 Python 標準除錯與追蹤機制。

Jython 2.7 支援 Python 2.7 為 set 型別提供的更多功能:

>>> {2,4,6,8,10}
set([2, 4, 6, 8, 10])
>>> # Create Empty Set
>>> set()
set([])
>>> s = {2,4,6,8,10}
>>> 3 not in s
True

Jython 支援 set 型別,它同樣支援 Python 的 set 所有功能:

>>> s.pop()
2
>>> s
set([4, 6, 8, 10])
>>> x.add(3)
>>> x.add(5)
>>> s.symmetric_difference(x)
set([3, 4, 5, 6, 8, 10])

現在我們來看 Java 整合,Jython 不是整合 Python 與 Java 唯一的方法,其它整合選項有 JPype (透過 JNI 嵌入 CPython) 和 Py4J (用遠端 socket 連線),但是,Jython 是獨一無二的,它支援 Java 物件如同使用 Python 物件般,反之亦然。

Python 語言的標準函式庫缺少能排序的 set,但 Jython 讓使用 Java 能排序的 set 實作(像 java.util.LinkedHashSet 確保插入順序及 java.util.TreeSet 維持內容本身的順序) 變容易,Listing 3示範如何使用。

Listing 3

相比 Java,一個些微差異是 Python 不用 new 關鍵字建立物件,取而代之,類別本身就是一個工廠,如 Listing 4 所示。

Listing 4

現在我們嘗試使用其它 Java 套件,Jython 的開發者是 Google Guava 函式庫容器的愛用者,廣泛地使用,特別是 MapMaker,函式庫中一個 concurrent map [譯註:這個我也放棄了,似乎沒聽過這的中譯]。

首先,我們需要下載 Google Guava 的 JAR 檔,然後放在我們的 CLASSPATH 路徑中 (在本例中,我們使用 Guava release 18.0),重啟 Jython 終端讓 CLASSPATH 的變動生效。

現在是一個好時機去嘗試 tab 自動完成,Jython 2.7 新加入的支援,有時候,在 Java 生態系中開發有一點不便:套件名稱很長,常常要吃力拼出來。有tab自動完成的支援,任何時候在終端上輸入文字時,您可以按 TAB 鍵取得可能自動完成清單 (如果有多個可能) 或直接自動完成填入 (若只有一個可能)。

所以先匯入:

>>> import com.google

然後,您可以輸入以下文字,接著按下 TAB 鍵

>>> d = com.google.c

於是您會得到:

>>> d = com.google.common

您最終可以完成下列:

>>> d = com.google.common.collect.HashBiMap.create(dict(one=1, two=2, three=3))

雙向對映的好處是會維護任何更新,如下所示:

>>> d.inverse()
{3: three, 2: two, 1: one}
>>> d.inverse()[4] = "four"
>>> d
{three: 3, four: 4, two: 2, one: 1}

練習:操作試算表

我們現在用更進一步的例子展示 Jython 與 Java 的深度整合。

我們的假設是要自動化的既有商業流程都仰賴試算表來呈現營運現況,雖然以試算表為基礎的流程的彈性已被證實,但它仍需手動處理且容易出錯。現今,這流程仰賴電子郵件、分享磁碟區、聚集與一些商業工具,聽起來很熟悉?

作為軟體開發者,我們知道有許多方法可以自動化這些商業流程,我們可以重寫然後不再使用試算表,但我們想保留試算表的優點:包含廣泛的使用率、彈性與容易使用,所以我們嘗試另一種方式:我們將繼續使用試算表,但提供更好的工具管理它們,我們將使用並整合下列工具:

  • Apache POI 可以以程式操作試算表 (Java函式庫)。
  • GitHub 為試算表進行版控,我們特別想利用其大量的 REST API 來儲存與取得試算表 (REST服務),因此,GitHub 作為我們可能用來管理(包含我們可能建立的)試算表的通用 REST 服務。
  • Requests 簡化 HTTP 以及特別是 RESTful 服務的使用,以利於使用 GitHub 的 REST API 去取得文件 (Python函式庫)。
  • Nosetests 支援單元測試 (Python函式庫)。
  • 客製化的 Python 程式黏合上述的東西,包含審核與公式計算。

首先,下載 Apache POI,撰寫本文時,最新的版本是 3.12,您需要從 POI 下載 poi、poi-ooxml、poi-ooxml-schemas 及 xmlbeans 等 JAR 擋到 CLASSPATH 路徑中。

下一步,您需要安裝需要的 Python 套件,安裝的支援是 Jython 2.7 的亮點,在過去,對 Python 開發者而言,Jython 先前的版本惱人的事情之一,Jython 從未完整支援 Python 的生態系,從 2.7 的版本開始,您可以欣然的獲得 Python 生態系帶來的優點,特別是 PyPI (Python Package Index) 有大量的 Python 套件,在 Jython 2.7 做這件事相當容易,因為廣受歡迎的 pip 工具已經包含在此版本中,這支援讓在應用程式中容易併入 Python 生態系的函式庫與 API 更新。

下述的指令將使用 pip 模組安裝 Nosetests 與 Requests 模組,-m MODULE 意指以命令列模式模式執行指定的模組,並處理後續的參數:

$ jython27 -m pip install nose requests

Java 與 Python 的相依性都已經處理好,那從哪裡開始?Python 語言能如此優雅是因為我們可以漸進式地在終端中探索問題與可能的解決方案。

假設我們有一個名為 hours.xslx 的試算表位於 GitHub 空間:https://github.com/jimbaker/poi 的最上層,我們可以用 https://github.com/jimbaker/poi/raw/master/hours.xlsx 取得試算表,我們可以在終端中嘗試 (url設成想要的試算表):

>>> import requests
>>> response = requests.get(url, stream=True)

我們將 reponse 寫到一個二進制檔案中 (因此檔案模式是"wb"),我們每次 512 bytes 逐次寫入檔案減少記憶體的使用量,writelines 函式接受一個iterator:

>>> f = open("hours.xlsx", "wb")
>>> f.writelines(respone.iter_content(512))
>>> f.close()

現在,我們用 POI 讀取儲存的試算表,注意 Jython 暗地將 Python 物件橋接成 FileInputStreamFileOutputStream,因此可以使用需要的 Java 函式或建構子:

>>> from org.apache.poi.xssf.usermodel import XSSFWorkbook
>>> workbook = XSSFWorkbook(open("hours.xlsx", "rb"))

我們可以對試算表做什麼?探索看看:

>>> dir(workbook)

最終,在終端中探索與 POI API 文件研究後,我們可能得到像 Listing 5 所示的程式來處理試算表:

Listing 5

process_workbook 函式可接受兩參數:pathcallback,注意,callback 是非必要參數,因為我們給予他一個預設值 None,然而,沒有像 Java 那樣指定靜態型別或 Scala 那樣推論型別,當我們提到程式碼的靜態分析 (或語法分析),是指檢驗程式碼文字,而不是執行它,我們 (編譯器或 IDE 等工具) 可以決定程式的某些屬性,像是變數的有效範圍、變數的型別、是否一致地使用型別,換句話說,程式是否有檢查型別,是否有部分程式碼不會被執行,因此是否可以移除?我們是否可以使用常數推導 (constant folding) 或內含 (inling) 等等。剛提及的部分,Jython 只支援靜態分析變數有效範圍的靜態 (CPython能進行部份的常數推導與無用程式碼碼移除,Python 3.5將有標準靜態型別註釋,作為混合動態型別與靜態型別的漸近型別系統的一部分)。

我們仍定義一個函式如果沒提供 callback,稱作 callback,這或許會搞糊塗,但其有效範圍在 process_workbook 函式內部,它不只是語法規範其有效範圍,事實上它是一個 closure,callback是有條件地被定義 [譯註:若外部沒提供就用內部的定義],因此這和我們在 Java 裡使用的方式相當不一樣。再一次,Python 展現出它確實是一個動態語言,任何對 process_workbook 的靜態分析只可能斷定 callback 可能是這個函式或不是,但是,注意 Jython 已經將程式碼編譯成 Java bytecode,所以看到 callback 的名稱是否設定到對應某個編譯過的函式主體,因此,條件式定義的開銷不過就是指定變數的開銷,這展現出 Jython 讓您在 Java 與 Python 之間來回選用合適的方式完成事情。

我們接著走訪整個試算表中每個表格的每一列與每個 cell,試算表集、表格與列物件都實作 java.lang.Iterable,讓 Jython 依序處理,也許不令人意外,Jython 的整合也能讓 Java 程式能用 for-each 迴圈的方式走訪 Python 的 iterables (或iterators)。

callback(cell) 的方式呼叫 callback 時,cell 被傳入,Jython 執行環境進行動態的型別檢查:確認callback 是否是一個可以被呼叫的物件?Python 有個簡單的規則:可被呼叫的物件都實作特別的函式 __call__。所有的方法 [譯註:這沒法也翻成函式,會變成無法區分] 都實作這特別的函式,但任意類別也可以實作,Python 總結這型別方式為 duck typing,這名稱來自若他看起來像鴨子、游泳像鴨子、像鴨子那樣呱呱叫,那它也許就是鴨子,Python假設您知道你在做什麼,讓您作主。

然而,如果某個物件沒提供 __call__ 函式,當程式執行時,Python 會拋出 TypeError 例外,當然,__call__ 本身也可能拋出例外。

現在我們定義一個 callback 可以審核試算表中的公式,非常類似 Excel 的公式,如果某個 cell 有個公式,這公式字串可以用 getCellFormula() 函式取得,注意 POI 的公式與 Excel 不同,因為沒有以 = 符號開頭。

因為 Python 除了函式外還支援屬性,Jython 進一步加強如何使用 Java 物件,您可以把 getter 和 setter 當屬性使用,忽略 getset,所以可以寫審核 callback 如下:

def print_if_hardcoded(cell):
try:
float(cell.cellFormula)
ref = CellReference(cell)
print ref.formatAsString(), cell
except:
pass

這裡我們看到一個動態語言與在 Python 中常用的 pattern:我們嘗試做某件事,然後捕捉所有例外,(這 pattern 稱做 “it is easier to ask forgiveness than permission” [譯註:這裡就視作專有名詞不翻譯了]),我們串起二個評估,取得公式字串 (如果不是,POI 會拋出 IllegalStateException 例外) 且試圖取得該字串的浮點數值 (若無法取直,Python拋出 ValueError 例外),如果嘗試失敗,表示這不是一個公式 (沒有您想找的公式)。

假設這程式存成 poi.py,我們可以用 jython -i poi.py 如 Listing 6 所示。

我們可以快速地建立一個腳本,用Requests下載試算表,套用剛寫的審核並儲存結果,然後可能作為一個REST API。

合併試算表

我們來看一個使用更多 POI 的例子,我們需要合併試算多個試算表成為一個,我們可以更進一步擴充,建立合併公式、提供格式化功能等,且可以更複雜如 Excel 能做的。

Listing 7 的程式 (可從download area下載),展示一種方法完成上述的事情,它善用 Python 2.7 新增的 argparse 函式庫開啟任意數量的試算表,然後將合併的結果寫到輸出的試算表,定義一個像這樣的主函式是符合 Python 語言習慣的。

當我們想對試算表每個 cell 做同樣的事情時,像 process_workbook 這樣的函式就很有用,其他情況,我們可能想處理特定的某些 cell,因此我們定義一個新的函式:get_cells 能取得指定範圍的 cell,像 A1:G8,或是參考的聯集,例如:A1,B1,C1,D1。

Listing 7

get_cells 函式是個 generator 函式,呼叫此函式會得到一個 iterator,每個迭代依序產出結果 (如 yield 關鍵字所標註),所以這是建構 java.lang.Iterator 相當方便的方式,但不需要明確捕捉每次呼叫與下個函式呼叫之間的狀態,generator 是 Python 程式相當常見的用法,特別是這樣簡化資料 (特別是大資料) 逐步地處理,Listing 8 (可從 download area 下載)展示 generator 的使用。

get_cells,我們可以很快地計算查詢的結果,馬上來試試,A1:G8 範圍的總和是多少?換句話說,等於計算試算表中 =SUM (A1:G8) 的結果。

定義下列的輔助函式 get_nums,然後使用內建的函式 sum

使用內建的函式 sum,答案很簡單是 sum(get_nums(get_cells(spreadsheet, "A1:G8")))

在 A1:G8 的範圍中是否有 cell 是有公式的呢?定義一個審核的函式,是先前我們已有的變形,然後使用內建的函式 any

def hardcoded_cells(cells):
for cell in cells:
try:
float(cell.cellFormula)
yield True
except:
yield False

答案就會是 any(hardcoded_cells(get_cells (spreadsheet, "A1:G8")))

我們目前完成的有定義初期高階的 Python API 操作試算表,組合一些可能可以用在試算表的函式,然而,我們能保留所有 Java POI 函式庫所提供的功能。

這引領我們到最後一個主題:我們有辦法確認我們的試算表通過承諾的測試嗎?複雜一點,假設我們建立一個持續整合的服務,像是 Jenkins,對試算表執行測試,也許作為 GitHub pull request 的一部分,我們該如何定義與執行我們的測試?Python 生態系在測試框架上有許多好的選擇,像是標準函式庫本身以及 unittest (xUnit測試風格的實作),但建築在 unittest 之上的 Nose 測試框架更廣為大家使用,因為它很容易使用。

例如。我們可能想確認 cross-tabulations 的正確性:某一列的小計應該等於某一行的小計,並考慮到數值精準度的問題,如 Listing 9 所示。

Listing 9

當這定義好,我們可以寫一個如 Listing 10 簡單的測試。

Listing 10

然後可以執行 Nose 去尋找並執行您的測試。Nose 遵循 convention-over-configuration (慣例優於設定) 的方式,意指它很容易使用,結果如下:

$jython -m nose
.
----------------------------
Ran 1 test in 0.033s
OK

第二列的每一個點代表所收集到的所有測試中的一個測試,直接加更多測試吧!

莫再回首

可預見地,隨著時間流逝,技術與語言特性都會演進,Jython 2.7 確實有些重要的捨棄,也許最需要注意的是,Jython 2.7 最低需求是 Java 7,另一個重要的是安裝程式不再支援使用其他的 JRE 產生 Jython 啟動器 (launcher),只使用 JAVA_HOME 中的 JRE。

Jython 3.5

Python 語言仍持續地發展,參考實作亦是,在您讀此文章時,CPython 將會釋出,某個時間點,規劃釋出 Jython 3.5,與 CPython 3.5 的釋出一起,值得提的一點是,Jython 2.7 基本上有與 Python 3.2 相同內在執行環境及 stdlib 的支援,但重要的成果將會在 Jython 3.5上,Python 3.5 一個熱切期盼的特性是選擇性的靜態型別,讓 Jython 有更好的 Java 整合。

但沒這麼快,Jython 2.7.x 將持續好一陣子,只要 Python 2.7 仍廣為使用中,Jython 開發團隊就會規劃維護 2.7.x,轉移並採用 Python 3 的進程相當緩慢,部分原因是 2.7 與 3.0 之間的差異過大。因為 Python 2.7 仍廣為使用,Jython 將在 Jython 2.7.x 的開發軸上規劃 time-based 的釋出,Jython 2.7.x 未來的釋出將專注在效能與整合等,Java 9 的釋出有許多動態語言上優化將帶來效能的改善。

即使不會這麼快到 Jython 3.5,但它已在進行。事實上,已經有個專屬 Jython 3.5 的開發分支,即便它仍在非常初期的階段。目前的目標是希望在未來二年釋出 Jython 3.5。

結論

Jython 2.7 提供豐富的工具,讓開發者在相同的 codebase 中結合二個廣受歡迎的生態系:Python 與 Java,在本文中,我們看到一些 Jython 新的功能,但還有其他相當棒的功能可以去探索,到 jython.org 下載它,然後關注後續的更新,每六個月將更新一次。

編輯:本文是持續探索 JVM 語言的系列文章的一部分,上一期,我們報導 Kotlin,下一期,我們介紹 Gosu,在工業界,一個同時用於前端與後端系統的 JVM 語言。

譯者的告別
雖然這次的翻譯我自己不是很滿意,一個原因是我對Python本身的熟悉度沒那麼高,因此無法完全理解作者某些地方的原意,另外是句子翻得不是很通順,不過,Java Magazine的2015年11–12雙月刊的所有文章總算是翻譯完畢,算是很有毅力地完成一件事了。

--

--