« 2006年03月 | メイン | 2006年05月 »

2006年04月22日

Jython入門

この文章ではJythonで「値がタブで区切られたデータファイルを読み込む」機能を実装することを目標としています。わかりやすく書こうと思っていますが、もしわかりにくいことがあればおきがねなくご連絡ください。なおEclipseとJythonのインストールは済んでいると仮定しています。 こちらにソースコードがあります。

Jythonを使うメリット・デメリット

Javaのみを用いてプログラムを書くことに比べて、 Jythonを使って一部分をPythonで書くことのメリットは大雑把に言うと次の3つです。

  1. Javaで書くよりもコードの量が少なくなる。
  2. Pythonで書かれ、動的に読み込まれている部分は、プログラムの再コンパイルをせずに実行内容を変更できる。
  3. 実行時にPythonコードを与えて実行させることが出来るので、プログラムを再起動することなく新しいコードの実行が出来る。

一方デメリットは次の3つです。

  1. 遅い。メモリ消費量も多い。
  2. JavaとPythonを知っている必要がある。特にチームで開発している場合や将来的に誰かに引き継ぐ必要がある場合はPythonを使える人を探すのに苦労するかも知れない。
  3. Pythonにはコンパイルや型チェックがないので、単純な綴り間違いで実行時エラーになる。Javaであればコンパイルエラーになるようなものも実行時エラーになる。

Pythonなどのスクリプト言語は「制約が少ないため実装が楽」という特徴を持っています。Javaのように型宣言をしたり、クラスを作ったりする必要がないため、新しいコードをさっと試してみたい、試行錯誤しながら最適な方法を調べたい、小規模なソフトウェアをさくっと書きたい、などのニーズに適した言語です。 一方、Javaは「制約が多いため使われ方をコントロールしやすい」という特徴を持っています。コンパイルがうまく行くかどうかで「整数を引数に取る関数に間違えて文字列を与えている」などのミスがないことを確認できます。またEclipseなどの統合開発環境を使うと「この関数の引数は文字列」などという情報がコーディング中に提示されるので、間違いを未然に防ぐことが出来ます。これらの機能のおかげで、特にGUI作成などの「他人が作ったコードを呼び出す必要のある作業」が楽になります。 異なった長所を持った2つの言語を束ね、適材適所で用いることで、どちらか片方で全部を実装する場合に比べて高い生産性を発揮することが出来ます。

JythonでHelloWorld

まずプロジェクトを作成します。パッケージエクスプローラを右クリックし、「新規」「プロジェクト」を選択し「Java Project」を作成してください。Eclipse用のPythonプラグイン「Pydev」などをインストールしている場合は「Pydev Project」を作成したくなるかも知れませんが、説明の都合上「Java Project」を作ったと仮定して解説をします。

次に、ビルドパス上にjython.jarを追加します。先ほど作成したプロジェクトを右クリックし「プロパティ」を選択してダイアログを表示してください。左のリストから「Javaのビルド・パス」を選びタブから「ライブラリー」を選べば のような画面になるはずです。図を参考に、jython.jarをビルドパス上に追加してください。なおjython.jarはJythonをインストールしたフォルダにありますが、自分のワークスペースなどにコピーしておいてもよいでしょう。

EclipseやJavaに慣れていない人のために、少し回り道をしてJavaで"Hello World"と表示するプログラムを作ってみます。慣れている人は飛ばして構いません。プロジェクトを右クリックして「新規」「クラス」を選びクラス名を「HelloJython」とすると下のようなコードが生成されます。

public class HelloJython {

}

mainと打ってCtrl+Spaceを押すとカーソル位置にmainメソッドが挿入されます。 また、sysoと打ってCtrl+Spaceを押すとカーソル位置に「System.out.println()」と挿入されます。「println()」の中に「"Hello Java!"」と書いて、実行し、「Hello Java!」と表示されることを確認してみましょう。

public class HelloJython {
	public static void main(String[] args) {
		System.out.println("Hello Java!");
	}
}

うまく動きましたか?それでは準備が出来たようなのでさっそくJythonを使ってみましょう。 まずは「PythonInterpreter」というクラスのインスタンスを作成します。 「Pyth」まで打ってCtrl+Spaceを押せば候補に出ます。それを選べばimport文も適切に作成され追加されます。PythonInterpreterのexecメソッドを使うと、文字列をPythonプログラムとして実行することが出来ます。下のサンプルでは「print 'Hello Python!'」というPythonプログラムを実行しているわけです。

import org.python.util.PythonInterpreter;

public class HelloJython {
	public static void main(String[] args) {
		System.out.println("Hello Java!");
		PythonInterpreter pyi = new PythonInterpreter();
		pyi.exec("print 'Hello Python!'");
	}
}

「Hello Python!」と表示されましたか?どうでしょう、二つの言語を股に掛けるのは、意外と難しくなかったのではないでしょうか?

JythonでHelloWorld(外部スクリプト起動編)

前の章のサンプルはJavaプログラム中に文字列定数としてPythonプログラムを記述していました。つまりPythonプログラムを変更すると再コンパイルが必要です。これではあまりメリットがないので、この章ではPythonプログラムを別のファイルに切り出したいと思います。

と言っても、PythonInterpreterのexecfileメソッドを使えば、指定したファイルをPythonプログラムとして実行することが出来るのでさほど難しくありません。注意する必要があるのはカレントフォルダがどこか、ということです。EclipseでJavaプログラムを実行した場合、カレントフォルダはプロジェクトフォルダになります。そこから見て、実行したいPythonファイルがどの位置にあるのかをきちんと指定してやる必要があります。下のサンプルは、プロジェクトフォルダの下に「script」という名前のフォルダを作り、その中に「init.py」という名前でPythonスクリプトを作った場合の呼び出し方です。

public class HelloJython {
	public static void main(String[] args) {
		PythonInterpreter pyi = new PythonInterpreter();
		pyi.execfile("script/init.py");
	}
}

実際にscript/init.pyを作成してみましょう。プロジェクトを作成するときに使った「新規」のメニューの中に「フォルダー」という選択肢があるはずなのでそれを使って作成します。次に、そのフォルダを右クリックして「新規」で「ファイル」を作成し、名前を「init.py」にします。Pydevをインストールしていれば、拡張子がpyのファイルはPythonスクリプトと見なされるので、色づけやオートインデント、補完などの機能を使うことが出来ます。便利なのでインストールしておくことをおすすめします。

ファイルが作成できましたか?それでは「print "Hello External Python!"」と書いて実行してみましょう。

PythonとJavaの間で値をやりとり

さて、前の章ではPythonとJavaを切り離したのですが、 実際はPython世界とJava世界の間で値をやりとりする必要がある場合がほとんどです。

PythonとJavaの間で変数の値をやりとりする方法はいくつもあります。 Pythonが主導権を握るのか、Javaが主導権を握るのか、 Python側からJava側に値を渡したいのか、Java側からPython側に値を渡したいのか、 関数呼び出しを使うのか、代入を使うのか、 の2x2x2で8通りと、importの1通りがあります。

  1. JavaからPythonInterpreter#setを使ってPythonのグローバル名前空間に渡す。
  2. JavaからPythonInterpreter#getを使ってPythonのグローバル名前空間から取る。
  3. JavaからPython関数を呼んで引数として渡す。
  4. JavaからPython関数を呼んで返り値を取る。
  5. Pythonからimportを使ってJavaクラスを取る。
  6. Pythonからフィールドに値を入れる。
  7. Pythonからフィールドの値を取る。
  8. PythonからJavaメソッドを呼んで引数として渡す
  9. PythonからJavaメソッドを呼んで返り値を取る。

こう書くと大げさですが、下のサンプルスクリプトでそのうちの3通りが使われています。まずPython側でimportを使ってJavaのHelloJavaクラスへの参照を取得し、 次にHelloJavaのpublicなフィールド「JAVA_STR」の値を参照しています。 次に、Python側でグローバル名前空間に置いたPY_STRという変数の値を、 Java側で「pyi.get("PY_STR")」とやって取得しています。

import org.python.util.PythonInterpreter;

public class HelloJython {
	static public String JAVA_STR= "String in Java World";
	public static void main(String[] args) {
		PythonInterpreter pyi = new PythonInterpreter();
		pyi.execfile("src/init.py");
		System.out.println(pyi.get("PY_STR"));
	}
}
import HelloJython
print HelloJython.JAVA_STR

PY_STR = "String in Python World"

Jythonで「ファイルを読み込んで行数を返す」

これまでの章で基礎的な話は終わったので、実用的なプログラムを作ることを考えて 「JavaでGUIを作成し、ユーザが指定したファイルをPython側で解析して、結果をJava側に返し、表示する」というプログラムを作ってみたいと思います。

GUIの作成にあたって、筆者はSWTを使うのが好きですが、SWTを使うための設定について話し出すとかなり脇道にそれることになるので、今回はAWT/Swingを使います。 まずは、Jythonとは関係がありませんが 「メニューをクリックするとファイル選択ダイアログが表示され、 ユーザにファイルダイアログを使ってファイルを指定させ、 そのファイル名を表示する」 と言うプログラムを作ってみたいと思います。

import java.awt.FileDialog;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.*;

public class HelloJython extends JFrame implements ActionListener {
	private FileDialog fd;

	public HelloJython() {
		fd = new FileDialog(this, "ファイルの読み込み", FileDialog.LOAD);

		// メニューの作成
		JMenuBar mnb = new JMenuBar();
		setJMenuBar(mnb);

		JMenu mnFile = new JMenu("File");
		mnb.add(mnFile);

		JMenuItem mniOpen = new JMenuItem("Open");
		mniOpen.setActionCommand("Open");
		mniOpen.addActionListener(this);
		mnFile.add(mniOpen);

		// 「右上の×印を押したら終了せよ」という設定
		setDefaultCloseOperation(EXIT_ON_CLOSE);
	}

	public static void main(String[] args) {
		JFrame frame = new HelloJython();
		frame.setSize(300, 200);
		frame.setVisible(true);
	}

	public void actionPerformed(ActionEvent arg) {
		if (arg.getActionCommand().equals("Open")) {
			// メニューが押されたらファイルダイアログを表示
			fd.setVisible(true);
			String dir = fd.getDirectory();
			String file = fd.getFile();
			if(file != null){
				// ファイルが指定されたならそれを表示
				System.out.println(dir + file);
			}
		}
	}
}

さて、ファイルダイアログで指定されたファイルの 名前を表示するところまでは出来ました。 それでは、このファイル名をPython側に渡すことを考えましょう。

方法はいくつかありますが、まずはPythonInterpreter#setを使って、 引数として渡したいものをグローバル変数にしてしまう方法を見てみましょう。 下記のソースコードでは、「pyi.set("filename", dir + file);」とやって ファイル名の情報をPython側のグローバル変数「filename」にしたあと、 「pyi.eval("countNumLine(filename)");」とやって、 「countNumLine」を呼び出した結果を取得しています。

import java.awt.FileDialog;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.*;

import org.python.util.PythonInterpreter;

public class HelloJython extends JFrame implements ActionListener {
	private FileDialog fd;
	PythonInterpreter pyi = new PythonInterpreter();

	public HelloJython() {
		fd = new FileDialog(this, "ファイルの読み込み", FileDialog.LOAD);

		JMenuBar mnb = new JMenuBar();
		setJMenuBar(mnb);

		JMenu mnFile = new JMenu("File");
		mnb.add(mnFile);

		JMenuItem mniOpen = new JMenuItem("Open");
		mniOpen.setActionCommand("Open");
		mniOpen.addActionListener(this);
		mnFile.add(mniOpen);

		setDefaultCloseOperation(EXIT_ON_CLOSE);
		
		pyi.execfile("script/init.py");
	}

	public static void main(String[] args) {
		JFrame frame = new HelloJython();
		frame.setSize(300, 200);
		frame.setVisible(true);
	}

	public void actionPerformed(ActionEvent arg) {
		if (arg.getActionCommand().equals("Open")) {
			// メニューが押されたらファイルダイアログを表示
			fd.setVisible(true);
			String dir = fd.getDirectory();
			String file = fd.getFile();
			if(file != null){
				// ファイルが指定されたならPython側に渡す
				pyi.set("filename", dir + file);
				PyObject result = pyi.eval("countNumLine(filename)");
				System.out.println(result);
			}
		}
	}

}
def countNumLine(filename):
    fi = open(filename)
    lines = fi.readlines()
    fi.close()
    return len(lines)

この方法は「グローバル変数を使う」という点が一部の人には嫌われそうです。 筆者は「だらしないけど楽」なほうが 「面倒だけどきっちりしている」のより好きですが、 楽な方法が常に使えるとは限らないので グローバル変数を使わない方法も紹介します。

下記のソースコードでは、 まずPython側の関数「countNumLine」をgetメソッドで取得しています。 次に、その関数に引数として渡すためのPythonオブジェクト(PyObject)を作成しています。 今回は渡すものが文字列なので、PyStringクラスのインスタンスを作成しています。 そしてそのオブジェクトを引数として関数オブジェクトの__call__メソッドを 呼び出しています。__call__メソッドは返り値をPyObject型で返します。

	public void actionPerformed(ActionEvent arg) {
		if (arg.getActionCommand().equals("Open")) {
			// メニューが押されたらファイルダイアログを表示
			fd.setVisible(true);
			String dir = fd.getDirectory();
			String file = fd.getFile();
			if(file != null){
				String filename = dir + file;
				PyString pyFilename = new PyString(filename);
				PyObject result = pyi.get("countNumLine").__call__(pyFilename);
				System.out.println(result);
			}
		}
	}

どちらの方法でも、返り値はPyObject型です。 今回はこれをSystem.out.printlnで表示していますが、 実際にはint型の変数に値を入れたい場合が多いでしょうね。 このようにJava側でPyObjectから値を取り出したい場合には、PyObjectをPyIntegerにキャストしてPyInteger#getValueメソッドを使います。

PyInteger result = (PyInteger) pyi.get("countNumLine").__call__(pyFilename);
int numLine = result.getValue();

しかし、明示的にキャストをするのは面倒です。 実はPython側からJava側の変数や関数にアクセスする際には、 Pythonオブジェクトから適切なJavaオブジェクトへの変換をJythonが行っています。 これを利用すれば、人間が頭を悩ませなくても値のやりとりが出来てしまいます。 この話は次章の後半で解説します。

タブ区切りのデータを読み込む

前の章で使ったcountNumLineの代わりに、 タブ区切りのデータを読み込んで2次元リストにして返す関数を作ってみましょう。 ソースコードは以下のようになります。

def loadTSVFile(filename):
    fi = open(filename)
    result = []
    for line in fi.readlines():
        items = line.strip().split("\t")
        result.append(items)
    return result

サンプルファイルは以下のような物とします。

a	1	2
b	2	3
c	3	1

loadTSVFileを呼び出して、System.out.printlnで表示してみましょう。

			PyObject result = pyi.get("loadTSVFile").__call__(pyFilename);
			System.out.println(result);

上のコードを実行すると「[['a', '1', '2'], ['b', '2', '3'], ['c', '3', '1']] 」と表示されます。Pythonのリスト(PyList)はJavaのString[][]などと異なり、リストの中身が全部表示されます。デバッグの際に便利です。

リストの要素を取得するのにはPyObject#__getitem__を使います。 result[1]に相当するのはresult.__getitem__(1)です。

			// __getitem__で取得
			System.out.println(result.__getitem__(1));
			System.out.println(result.__getitem__(1).__getitem__(2));

これはメソッド名が長いのでちょっと面倒です。 そこであらかじめString[][]に値を移し替えて見ましょう。

			// String[][]に変換
			String[][] strResult;
			int size = result.__len__ ();
			strResult = new String[size][];
			for(int i = 0; i < size; i++){
				PyObject line = result.__getitem__(i);
				int lineSize = line.__len__();
				strResult[i] = new String[lineSize];
				for(int j = 0; j < lineSize; j++){
					PyString item = (PyString)(line.__getitem__(j));
					strResult[i][j] = item.toString();
				}
				
			}
			System.out.println(strResult[1]);
			System.out.println(strResult[1][2]);

値の取得は短くなりましたが、移し替える部分がさらに面倒です。 こんな面倒なことをしないといけないのだったらPythonを使うメリットがないのでは? いえいえ、そんなことはありません。 Pythonスクリプト中で型のはっきりしたJavaの変数に代入を行うと Jythonが自動的にPyObjectから適切な型への変換を行ってくれます。 これを使えばかなり楽です。

			// Jythonに変換させる
			pyi.set("filename", filename);
			pyi.set("this", this);
			pyi.exec("this.data = loadTSVFile(filename)");
			System.out.println(data[1]);
			System.out.println(data[1][2]);

dataはpublicでString[][]のフィールドですが、 メソッド呼び出しの場合も同じように適切な型への変換が行われます。

Future work

駆け足でJythonを使ってタブ区切りのデータを読み込むまでを解説しましたが、いかがでしょうか?ご意見、ご感想、ご質問がございましたらお気兼ねなくご連絡ください。

評判がよければ、今後、JavaプログラムにJythonコンソールをつけて 対話的実行が出来るようにする方法などについて書いていきたいと思っています。

Javaでタブ区切りのファイルを読み込む

Jythonで書いたものと比較するために、Javaでタブ区切りのファイルを読み込む関数を実装しました。なんだかやたらとまどろっこしい気がしますが、もっとスマートな書き方はないのでしょうか…。故意に悪い書き方はしていないつもりなのですが、これではJavaに不利すぎますね。

	public String[][] loadTSVData(String filename) {
		ArrayList aList = new ArrayList();
		String[][] result = null;
		try {
			FileReader fi = new FileReader(filename);
			BufferedReader br = new BufferedReader(fi);

			while (br.ready()) {
				String line = br.readLine();
				ArrayList lineList = new ArrayList();
				StringTokenizer st = new StringTokenizer(line, "\t");
				while (st.hasMoreTokens()) {
					lineList.add(st.nextToken());
				}
				aList.add(lineList);
			}

			br.close();
			fi.close();
			int size = aList.size();
			result = new String[size][];
			for(int i = 0; i < size; i++){
				ArrayList lineList = (ArrayList) aList.get(i);
				int lineSize = lineList.size();
				result[i] = new String[lineSize];
				for(int j = 0; j < lineSize; j++){
					result[i][j] = (String) lineList.get(j);
				}
			}
			                          
		} catch (IOException e) {
			e.printStackTrace();
		}
		return result;
	}

同じ処理をJythonで書くとこうなります。

def loadTSVFile(filename):
    fi = open(filename)
    result = []
    for line in fi.readlines():
        items = line.strip().split("\t")
        result.append(items)
    return result

前者のコードはファイルからArrayListに読み込んだ後でString[][]に移し替えてから返していますが、後者はファイルからPyListに読み込んだ後、そのまま返しています。なので「同じ処理」は正確には前者のコードからファイルをクローズした後の10行を削ったものになります。

ただ、返り値は型がPyObjectですが、すでに型の定まっているJavaクラスのフィールドに代入したりメンバ関数へ引数として与えたりした場合は自動で変換されるので、うまく使えばそもそも変換する必要がないかも知れません。

2006年04月06日

Pythonで「成分解析」(ハッシュ値を乱数シードに使う)

最近流行っているらしい成分解析。「こういう適当な結果を返すのに乱数を使いたいが、普通に乱数を使ってしまうと解析のたびに違う結果が帰ってきてしまう」「どうやればこういうプログラムを作れるのだろう」という話をどこかで小耳に挟んだので、サンプルを作ってみました。

一言で言ってしまうと、タイトルに入れた「ハッシュ値を乱数シードに使う」で済んでしまいます。もっと詳しく言うと、「与えられた名前などの情報を適当な関数で整数に変換し、その値を用いて乱数の初期化を行う」とでもなるでしょうか。「適当な関数」は、同じ文字列を与えると同じ整数値が返ってくるような関数を使います。たとえば「最初の1文字の文字コードを返す」でも構いません。ただ、その場合、最初の文字が同じ人は同じ結果になります。もちろんたまたま全然似ていない名前の人が同じ結果になる場合もあります。これらの問題を避けるためには「文字列全体によって値が決まり」「なるべく広い範囲の値にまんべんなく散らばる」という特徴が欲しいところです。

さて、実はそういう特徴を持った関数はすでにハッシュ(連想配列、辞書)というデータ構造の実現のために多くの言語で実装されており、Pythonに至ってはhashという組み込みの関数まであります。これを使って乱数を初期化してやればいいわけです。実はPythonの乱数モジュールは初期化関数に整数でないものが渡されると、勝手にhashでハッシュ値を求めて使うようになっています。つまり、やるべきことは非常に少なく、

import random

name = "西尾泰和"
random.seed(name)

これで終わり。生年月日などがある場合はタプルにくくって("西尾泰和", 1981, 7, 23)と行った形でseedに与えることができます。Pythonのタプルはハッシュ化可能なので。

さて、駆け足で解説を書いてきましたが、ソースコードはこちら。50行程度で

# -*- coding: cp932 -*-
#
# 成分表示の簡単な実装
import random

seibunStr = """
ビタミン タンパク質 脂肪
愛 希望
けだるさ 思いつき
"""

seibunList = seibunStr.strip().split()

name = "西尾泰和"
random.seed(name)

# いくつの成分からなるか (3~5こ)

numSeibun = random.randint(3, 5)

# 成分を選んで、割合も決める

result = []
sumValue = 0
for i in range(numSeibun):
    s = random.choice(seibunList)
    seibunList.remove(s)
    v = random.random()
    sumValue += v
    result.append((v, s))

result.sort()
result.reverse()

# 値の合計が100%になるように調整

sumPercent = 0
for (i, (v, s)) in enumerate(result):
    percent = int(100 * v / sumValue)
    sumPercent += percent
    result[i] = (s, percent)

result[0] = (result[0][0], result[0][1] + 100 - sumPercent) # 丸め誤差をトップに足す

# 表示
print name, "は"
for (s, p) in result:
    print "%d%%の%s" % (p, s)

print "で出来ています。"