移植性の高い Python スクリプトを書く

以前 Lua の処理系 LuaJIT が速くて羨ましいという話を書いたが,最近は Python でも JIT コンパイラ PyPy の性能向上が著しいようだ.どれぐらい速いかというと,以前実験した PA-I(機械学習)で LuaJIT での実行速度を上回るぐらい*1Python はもともとスクリプト言語の中では実行速度が速い方だったが,PyPy の急速な性能向上によって PerlRuby といった競合言語に対して(実行速度の点で)差を広げつつあるようだ.

そういうわけで,最近スクリプトPython で書く機会が増えている.Python でコードを書く上でやっかいなのは(まともな)ワンライナーが書けないこと*2と,(処理系のバラつきに起因する)移植性の問題である.前者はどうにもならないので,perl / ruby / sed + awk などで回避することになるが,後者は公開する可能性がある(開発環境と別環境で実行されうる)コードを書く際には避けては通れない.

公開するコードの価値に自信があれば,処理系の更新を促進することも狙って敢えて最新の処理系のみで動く実装を公開するという選択もありかもしれないが.ユーザの視点に立つと,一つのプログラムを使うためにわざわざ処理系をインストールするというのは大きな負担になる.普段,外部ライブラリにあまり依存しないコードを書くよう留意してパッケージングしているので,特定の処理系への依存はなるべく避けたいところ*3

一年ほど前の記事だけど,

をみると,現在想定される python のバージョンは 2.4 (Dec 2004), 2.5 (Sep 2006), 2.6 (Oct 2008), 2.7 (Jul 2010), 3.2 (Feb 2011) 辺りのようだ.python 2.4 がまだ現役かというと,自分が公開した Python スクリプトが動かないと報告してくれたユーザがみんな python 2.4 を利用していたので,まだ 2.4 は外せないかな.ちなみに自分のログイン環境には 2.3 が現役なところもあった.言語処理系に限らずソフトウェアのバージョンは,ハードウェアの陳腐化(+ OS ディストリビューションごとの最新版への選好性)に左右されるので,少し余裕をもってみておく必要がある.Python では,2.X系列と3.X系列の間にはよく知られるように意図的な断絶があるが 2to3 である程度互換性を担保できそうなので(公開するコードに関しては)現時点だと 2.4 辺りを想定して(3.X で廃止された機能を使わないよう,必要最小限の言語仕様で)コードを書くのがよさそうだ.

そこで,python の各版における新機能を過去にさかのぼって,何が新機能として追加されたか(どんな機能をまだ使わない方が良いか)調べてみた.python の各版の新機能については,以下のページに良くまとまっている.

使用頻度が高く影響が大きそうな新機能を時系列順でまとめてみる(Python 2.4 以降の共通仕様で,(2.5 以降の)各版の新機能をどう実現できるかまとめようかと思ったが,すぐに陳腐化して役に立たなくなりそうなのでやめた).Python 初心者から見た主観的な取捨選択になっているので,興味がある人は自分で確認した方が良い.太字は特に自分にとって特に必要な機能.

Python 2.4 に注目すると,bool 型,ジェネレータ式,sorted/reversed, logging, subprocess は間に合っているが,自分が日常的に使う三項演算子や collections.defaultdict が含まれていない.三項演算子は X = B if A else C の代わりに X = A and B or C と書けば良いが(B が偽となる場合を除く)defaultdict はキーの存在を判定する条件節を追加する必要があり煩わしい.また C++ でもそうだけど,オプション解析は Python でも標準化への苦労の跡が見られ,2.3 で導入された optparse がもう obsolete になっている.移植性を考えると枯れた getopt に回帰することになるのか*4.あと Ruby に馴染みがある身としては,extended sequence/iterable unpacking が 3.0 まで無い(から,公開用コードでは当分使えない)のはストレスが溜まる.2.5 が行き渡るまでは,不自由が続きそうな悪寒.
こうしてみてみると,救いだと思うのは文字コード周りの変更が少ない点(3.X では Unicode の扱いが変わっているが).今の仕様が日本語を扱う上で十分使いやすいかというとそうでもないのだけど,perl 5.6 -> 5.8 (Jul 2002) や,ruby 1.8 -> 1.9 (Jan 2009) ような大きな変更があると,一時的に使いづらくなってしまう.ruby の 1.9 への移行が終わるまではこのままでいてもらえると,助かるのだけど(そうでないと,忘却の彼方にある perl を使わないといけなくなる).
自分のコードを公開するようになってから,プログラミング言語は新機能よりも,現在の仕様がどれだけ枯れているかに興味を持つようになった.

*1:MacBookAir (Mid 2011) で PyPy (1.8): 7.3s < LuaJIT (2.0.0 beta9): 7.8s.ただし,PyPy は訓練例の読み込み部分で速度を稼いでいて,学習部分は遅い.実験には PyPy 1.8 Mac OS/X binary (64bit) を使い,PA-I の Python 実装には前の実装で数値配列を array で書き直したものを用いた.[追記] 内包表記をやめて for-loop にしたら,学習部も LuaJIT 並になって全体の実行時間は 5.2s になった.メモリ使用量が 230MB->130MB と改善されていたので GC がうまくいくようになったかもしれない.lua正規表現で素性を取り出すのをやめて string.byte と string.sub を使うようにしたら 6.0s ぐらいまでは速くなった.スクリプト言語では,組み込み関数を使う方が(自分で処理を書き下すより)効率が良いと言われるが,JIT を使う場合は必ずしもそうではないようだ.まあ,スクリプト言語で処理速度を追求することほど不毛なことはないので,この辺りはほどほどにしておきたい.

*2:これは主にインデント依存の文法のためだが,Perl/Ruby などと比べると,特殊変数がなかったり if 文の条件節で代入できなかったり(正規表現などで二度手間になる)後置ifがなかったりと,コードの短さが期待される用途にはあまり向いていない印象.

*3:というか,ここ最近,自分が実装した Python スクリプトが別環境で動かないという報告を何度ももらったので意識せざるを得なくなった.自分のコードを使ってくれる貴重なユーザに,(コードの変更で簡単に対応できるのにも関わらず)処理系を更新してくれとは言えない.

*4:[追記] optparse も legacy code をサポートするために現状維持されるようだ.以下引用.The older module is still being kept available because of the substantial amount of legacy code that depends on it. また,optparse becomes redundant, but there are no plans to remove it because there are many scripts still using it, and there’s no automated way to update these scripts.