PLY (Python Lex-Yacc) で作る Algol 60 処理系 第4部

2009.2.17 (鈴)

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 のどれでも動くようにしました。 基本的な手順は次のとおりです。

  1. Python 3.0 付属の 2to3 ユーティリティで各ソースを Python 3.0 用に変換する。例:
    $ 2to3 -w Interpreter.py
  2. 変換したソースをそれ以前のバージョンの Python でも動くように手で書き直す。

以下,手による書き直しについて,構文レベルの対応と,オブジェクト・レベルの対応に大別して説明します。 繁雑さを避けるため,Python 2 と書いて Python 2.3.5 以降 3.0 の前まで, Python 2.5 と書いて Python 2.5 以降 Python 2.6 の前までを表すことにします。 他についても同様の略記をします。

18. 構文レベルの Python 2/3 両対応

18.1 tryexcept

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.pydo_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 ではいくつかの組込み関数がモジュールの中に移動しています。 本インタープリタでは reduceintern が該当します。 Python では関数はファーストクラスのオブジェクトですから,互換性のための別名を単なる代入で定義できます。

try: intern
except NameError: intern = sys.intern # for Python 3.0

別の書き方として fromimport が使えます。

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 両対応にするには次のようにします。

  1. 次のコードをファイルの先頭付近に置く。
    try: raw_input
    except NameError: raw_input = input # for Python 3.0
    
  2. input(prompt) という式を eval(raw_input(prompt)) にする。

ユーティリティ 2to3input(prompt)eval(input(prompt)) に変換します。 これをさらに Python 2 にも対応できるように inputraw_input に換え,raw_input を Python 3.0 用に定義してやるわけです。

19.3 辞書の has_key メソッドと in 演算

Python 3.0 の辞書オブジェクトには has_key メソッドがありません。
辞書オブジェクト dd に対し, ユーティリティ 2to3dd.has_key(key)key in dd に変換します。

辞書に対する in 演算は Python 2.1 ではサポートされておらず,Python 2.2 からの機能です (ただし,当時は True, False がなく,結果は 1 か 0 でした)。 ここで対象としているのは Python 2.3.5 以降ですから,単純に 2to3 の変換結果を使うことができます。

つまり,そもそも has_key を使い続けていた自分が最大 7 年ほど時代遅れだったということですね >_<
ちなみに古さの上限として 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 ではリストを1回ムダに生成しています。 もしもこのムダをなくしたいときは,list のかわりに,Python 2 では恒等関数,Python 3 では list 関数として はたらくような関数を定義して適用するようにします。
もっとも,いずれにせよ,今回扱っているのは多次元配列の添え字の短い並びですから,大勢には影響しません。 しかるに長大なリストの場合は,イテレータのまま扱うかどうか,つまり遅延評価的に処理するかどうかが 大きな効率の違いになりますから,実用的な 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/algol60lib/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 自体の使い方は変わりありません。


目次へ


Copyright (c) 2009 OKI Software Co., Ltd.