about 2 years ago

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.

Jython 2.7: 結合Python與Java

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

在廣大社群、強健的生態系與穩健的語言加持下,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.
$ jython
Jython 2.7.0 (default:. . ., Apr 29 2015, 02:25:11)
[Java HotSpot(TM) 64-Bit Server VM (Oracle Corp.)]
Type "help", "copyright", "credits" or "license"
for more information.
>>>

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

Listing 2.
>>> d1 = {'one':1, 'two':2, 'three': 3}
>>> d1['three']
3
>>> # Equivalent construction by using keywords
>>> # Note that '#' introduces a comment, including in the console
>>> d2 = dict(one=1, two=2, three=3)
>>> d2['one']
1
>>> d1 == d2
True
>>> len(d1) # length of d1
3
>>> # Note that there is no construction equivalent using keywords,
>>> # because keywords are limited to strings that would also be
>>> # valid Python identifiers.
>>> inverted = {1: 'one', 2: 'two', 3: 'three'}

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支援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.
>>> from java.util import TreeSet
>>> clangs = TreeSet(["c", "python", "ruby", "perl", "javascript"])

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

Listing 4.
>>> jvmlangs = TreeSet(["java", "python", "groovy", "scala", "ruby", "javascript"])
>>> clangs | jvmlangs # set union

[c, groovy, java, javascript, perl, python, ruby, scala]
>>> clangs & jvmlangs # set intersection

[javascript, python, ruby]

現在我們嘗試使用其它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語言能如此優雅是因為我們可以漸進式地在終端中探索問題與可能的解決方案。

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.
# traverses cells in a workbook,
# calling a callback function on each cell

from contexlib import closing

def process_workbook(path, callback=None):
  if callback is None:
    def callback(cell):
      print cell,

with open(path, "rb") as file:
  with closing(XSSFWorkbook(file)) as workbook:
    for sheet in workbook:
      for row in sheet:
        for cell in row:
          callback(cell)

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之間來回選用合適的方式完成事情。

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所示。

Listing 6.
>>> process_workbook("example.xlsx", print_if_hardcoded)
A1 42
A2 47

我們可以快速地建立一個腳本,用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。

def get_cells(sheet, ref):
  for row_idx, col_idx in referred(ref):
    row = sheet.getRow(row_idx)
    if row is not None:
      cell = row.getCell(col_idx)
      if cell is not None:
        yield cell

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

NUMERIC_CELLS = { Cell.CELL_TYPE_FORMULA, Cell.CELL_TYPE_NUMERIC }
def get_nums(cells):
  for cell in cells:
    if cell.cellType in NUMERIC_CELLS:
      yield cell.numericCellValue

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

在A1:G8的範圍中是否有ell是有公式的呢?定義一個審核的函式,是先前我們已有的變形,然後使用內建的函式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.
from nose.tools import assert_almost_equals

def assert_crosstab(spreadsheet, range1, range2):
  assert_almost_equals(sum(get_nums(spreadsheet, range1)), sum(get_nums(spreadsheet, range2)))

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

Listing 10.
finance_wb = XSSFWorkbook("financials.xlsx")
main_sheet = finance_wb.sheetAt(0)

def test_financials():
  assert_crosstab(main_sheet, "A5:G5", "H1:H5")

然後可以執行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雙月刊的所有文章總算是翻譯完畢,算是很有毅力地完成一件事了。

← 環境與關係注入:新的Java EE工具箱 用非同步Servlets實現Long Polling →