PLY (Python Lex-Yacc) で作る Algol 60 処理系 第4部
2009.2.17 (鈴)- 第1部
- 1. はじめに
- 2. Algol 60 とは
- 3. PLY の導入
- 4. 字句解析
- 5. 構文解析
- 6. ここまでのまとめ
- 第2部
- 7. 名前の解決
- 8. インタープリタ - 文の単方向リンクと goto 文
- 9. インタープリタ - 手続きとディスプレイとスタック
- 10. インタープリタ - 式の中からの大域脱出
- 11. インタープリタ - call-by-name の実現
- 12. インタープリタ - のこりの言語要素
- 第3部
- 13. インタープリタへの標準関数の組込み
- 14. メイン・スクリプト algol60
- 15. インタープリタの使い方
- 16. ここまでのまとめ
- 第4部
- 17. PLY-3.0 と Python 2/3 互換性
- 18. 構文レベルの Python 2/3 両対応
- 19. オブジェクト・レベルの Python 2/3 両対応
- 20. その他の変更
- algol60-0.7.tar.bz2 (56117 バイト)
Python (2.3.5 から 3.0.1 まで) による Algol 60 インタープリタ 0.7 版 (PLY 3.0 同梱)
- Lexer.py 字句解析器
- Types.py 各構文要素を表す型など
- Parser.py 構文解析器
- Interpreter.py インタープリタ本体
- Prelude.py 標準関数の実装
- algol60 メイン・スクリプト
- Copyright.txt 著作権表示
17. PLY-3.0 と Python 2/3 互換性
2006年 2月 6日 (現地時間) に PLY-3.0 がリリースされました。 これは Python 2.2 から Python 2.6 および Python 3.0 をすべてサポートしています。
そこで algol 60 インタープリタも PLY-3.0 を採用するとともに,必要な改造を行い,Python 2.3.5 から Python 2.6.1 および Python 3.0.1 のどれでも動くようにしました。 基本的な手順は次のとおりです。
- Python 3.0 付属の 2to3 ユーティリティで各ソースを Python 3.0 用に変換する。例:
$ 2to3 -w Interpreter.py - 変換したソースをそれ以前のバージョンの Python でも動くように手で書き直す。
以下,手による書き直しについて,構文レベルの対応と,オブジェクト・レベルの対応に大別して説明します。 繁雑さを避けるため,Python 2 と書いて Python 2.3.5 以降 3.0 の前まで, Python 2.5 と書いて Python 2.5 以降 Python 2.6 の前までを表すことにします。 他についても同様の略記をします。
18. 構文レベルの Python 2/3 両対応
18.1 try…except
Python 2 と Python 3 の構文レベルでの非互換性のうち,とりわけ深刻なのは
L2 Lisp の Python 2.5 & 3.0 への移植
§3.1 「Python 2.5 と Python 3.0 のはざまで」
でも説明した try 文の except 節の構文の違いです。
Python 2.5 までは
except SomeException, ex:
……
と書き,Python 3.0 では
except SomeException as ex:
……
と書きます。両者の橋渡しとして Python 2.6 では両方の構文をサポートします。
単一のソースで Python 2 から Python 3 までのすべての Python に対応するには,次のようにします。
except SomeException:
ex = sys.exc_info()[1]
……
18.2 print
Python 3.0 では print 文が単なる組込み関数 print になっています。
しかし,その引数が単純な一つの式 an_expression ならば, 2to3 の変換結果のとおりに
print(an_expression)
とすれば,Python 2 と 3 で互換性があります。
Python 2 の print
文の典型的な引数は,左項が文字列,右項がタプルである剰余演算 (例: "%s - %s" % (x, y))
ですから,これに該当します。
ただし,この類推で,改行だけをする無引数 print 文について
2to3 の変換結果 print() をそのまま使うと,Python 2 と 3 で動作が異なります。
前者は空タプル () を印字し,後者は改行をします。
最初の意図どおり,単に改行をするときは,
sys.stdout.write("\n")
とすれば Python 2 と 3 両対応にできます。
標準エラー出力への print 文,例えば,
print >>sys.stderr, "usage: algol60 source_file_name"
を Python 2 と 3 両対応にするには,write メソッドを使います。
このとき改行文字を陽に追加することに注意します。
sys.stderr.write("usage: algol60 source_file_name\n")
Python 2 で print 文がカンマで終端しているのを両対応にするには, 適切に空白を出力するようにコードを組む必要があります。 Interpreter.py の do_print_procedure メソッドでは,
def do_print_procedure(self, args):
if not self.in_new_line:
print
for e in args:
value = self.eval_expression(e)
print value,
self.in_new_line = False
を次のように変更しました。
def do_print_procedure(self, args):
if not self.in_new_line:
sys.stdout.write("\n")
is_first = True
for e in args:
if is_first:
is_first = False
else:
sys.stdout.write(" ")
value = self.eval_expression(e)
sys.stdout.write(str(value))
self.in_new_line = False
19. オブジェクト・レベルの Python 2/3 両対応
19.1 モジュールに消えた組込み関数
Python 3.0 ではいくつかの組込み関数がモジュールの中に移動しています。 本インタープリタでは reduce と intern が該当します。 Python では関数はファーストクラスのオブジェクトですから,互換性のための別名を単なる代入で定義できます。
try: intern except NameError: intern = sys.intern # for Python 3.0
別の書き方として from … import が使えます。
try: reduce except NameError: from functools import reduce # for Python 3.0
19.2 input/raw_input
Python 2 の組込み関数 input は Python 3.0 に直接該当するものがありません。
Python 2 の組込み関数 raw_input は Python 3.0 の input に相当します。
ですから,Python 2 の input を Python 2/3 両対応にするには次のようにします。
- 次のコードをファイルの先頭付近に置く。
try: raw_input except NameError: raw_input = input # for Python 3.0
input(prompt)という式をeval(raw_input(prompt))にする。
ユーティリティ 2to3 は input(prompt) を
eval(input(prompt)) に変換します。
これをさらに Python 2 にも対応できるように input を raw_input
に換え,raw_input を Python 3.0 用に定義してやるわけです。
19.3 辞書の has_key メソッドと in 演算
Python 3.0 の辞書オブジェクトには has_key メソッドがありません。
辞書オブジェクト dd に対し,
ユーティリティ 2to3 は dd.has_key(key) を
key in dd に変換します。
辞書に対する in 演算は Python 2.1 ではサポートされておらず,Python 2.2 からの機能です (ただし,当時は True, False がなく,結果は 1 か 0 でした)。 ここで対象としているのは Python 2.3.5 以降ですから,単純に 2to3 の変換結果を使うことができます。
ちなみに古さの上限として Python 2.3.5 にこだわるのは,これが一応まだ現役の Mac OS X "Tiger" の標準添付の /usr/bin/python のバージョンだからです。
19.4 イテレータと list
Python 3.0 の map 関数は,戻り値としてリストではなくイテレータを返します。 これにより,遅延評価的な効率良い計算が可能になります。 しかし,この戻り値にスライス演算のようなことはできません。
そのため,ユーティリティ 2to3 は,map の戻り値に list を適用して,強制的に戻り値をリストにします。
ii = list(map(operator.sub, subscript, self.offsets))
…
jj = list(map(operator.mul, ii[:-1], self.lengths[1:]))
これは Python 2 でもそのまま通用します。 さしあたり 2to3 の変換結果を使えば十分です。
もっとも,いずれにせよ,今回扱っているのは多次元配列の添え字の短い並びですから,大勢には影響しません。 しかるに長大なリストの場合は,イテレータのまま扱うかどうか,つまり遅延評価的に処理するかどうかが 大きな効率の違いになりますから,実用的な Python 2/3 両対応は困難になります。 トリビアルな非互換性だけではない本当の新言語としての Python 3 の片鱗がそこに見えているわけです …が,その議論はまたの機会にします。
19.5 消えた long
Python 3.0 には型オブジェクト long がありません。
無限多倍長整数と有限長の整数は完全に統合され,int で総称されています。
Interpreter.py では long を次の形式の関数適用でだけ参照していました。
isinstance(exp, (int, long))
そこでこれを
isinstance(exp, INTEGERS)
に書き換え,コードの先頭に次を追加します。
try: INTEGERS = (int, long) except NameError: INTEGERS = int # for Python 3.0
19.6 エンコーディング指定付きの open
Python 3.0 の組込み関数 open は,開こうとするファイルの文字エンコーディングを,キーワード引数 encoding で決定します。 その引数がないとき,Python 2 との互換性の観点からはバイト透過 (≒ 'latin-1' エンコーディング) にすべきところですが,残念ながら,そうなっていません。 Mac OS X では 'X-MAC-JAPANESE' (Python 3.0 では単に 'shift_jis' の別名) がエンコーディングとして使われます (locale 依存でさえありません)。
これは PLY の実装方法とはそぐいません。PLY は構文解析器の Python ソースを open
で開いて読みます。
Python 2 ならばとにかくバイト透過に読むわけですが,Python 3 では,たとえソース中に
-*- conding: utf-8 -*- があっても無視して,既定のエンコーディングで読もうとします。
ソースが既定のエンコーディングと一致しない場合,たいてい,そのエンコーディングからみて不正であるような
マルチバイト列に遭遇して,エラーに終わります。
これに対する単純で一般的な解決方法はありませんが,ここでは,テキストがつねに UTF-8
で書かれていると割り切った仮定をし,組込み関数 open の定義を,つねにキーワード引数
encoding='utf-8' が与えられるようにすげかえます。
将来の更新に追従しやすいように,PLY のコードには手を入れません。
try: file except NameError: # for Python 3.0 import builtins __open = open # UTF-8 テキストを仮定する def my_open(file, *args, **kwd): kwd['encoding'] = 'utf-8' return __open(file, *args, **kwd) builtins.open = my_open
黒魔術を使った言い訳…になりますが,Python 3.0 のマルチバイト文字の扱いは,どこかピント外れです。 過去との互換性があるわけでもなければ,実際にマルチバイト文字を使う人にとって便利というわけでもありません。 誰が望んでいた,誰のための仕様なのか不明です。 同類の言語のあいだで比べたとき, 少なくともこの点については,設計の全過程で 常時 実用性のテストにさらされてきただろう Ruby 1.9 に劣ります。
ちなみに Ruby 1.9 で上記と同様のことは
Encoding.default_external = Encoding::UTF_8 で健全かつ完全に実現できます。
20. その他の変更
Algol 60 インタープリタ本体の著作権表示と,同梱の PLY の著作権表示の混同を避けるため,
lib ディレクトリの中で両者を別々のフォルダ (lib/algol60 と
lib/ply-3.0) に分けました。メイン・スクリプト bin/algol60
では,これに対応するため sys.path を両者に通すようにしました。
prog = sys.argv[0] prog_path = os.path.dirname(prog) for lib_name in "ply-3.0", "algol60": lib_path = os.path.join(prog_path, "..", "lib", lib_name) lib_path = os.path.abspath(lib_path) sys.path.insert(0, lib_path)
Algol プログラム実行時に発生した例外の報告を標準出力ではなく,標準エラー出力に出すようにしました。
try: os.chdir(lib_path) interp.run(source_text) except (SyntaxError, SemanticsError): ex = sys.exc_info()[1] sys.stderr.write("%s: %s\n" % (ex.__class__.__name__, ex)) sys.exit(1)
メイン・スクリプト bin/algol60 自体の使い方は変わりありません。


