YAMAGUCHI::weblog

海水パンツとゴーグルで、巨万の富を築きました。カリブの怪物、フリーアルバイター瞳です。

Python 2からPython 3への移行

はじめに

こんにちは、Python界のNintendo 3DSです。結婚式2次会で当たりました。さてPython 3.2がリリースされて、Python 3.3のリリーススケジュールが発表され、いよいよPython Language Moratoriumも終了が近づいてきました。

C/APIではもうモラトリアムは破られているということなので、ぼちぼちPython 3への移行を考えていたらいくつか記事があったのでご紹介します。

参考


今回はMike Bayer(zzzeek)のエントリを翻訳しました。

zzzeekのPython 3移行ガイド

ちょっと前に、Ben*1が「あのさ、Python 3への移行方法のハウツーってネット見ても見つからないんだけど?」と聞いてきました。まあ3つか4つくらいは基本的なやり方とか大枠とか、GuidoがPyCon '10(か'09かたしかそのへん)でやったようなネタを書いたブログエントリが見つかるだろうと思ってGoogleで調べてみました。そしたら驚くことに、2to3ツールとGuidoのオリジナルガイド以外にはほとんど何もなかったんです。
というわけで、これからPyPIにあるSQLAlchemyとMakoのPy2kとPy3k両方に対応したリリース版を作るまでに行ったやり方をご紹介します。

1. 最低限Python 2.6で確実に動くようにする

Python 2.6か2.7でテストを -3 フラグ付きで実行してください。警告が1つもでないようにして下さい。たとえば、こんなくだらないプログラムがあったとして:

def foo(somedict):
    if somedict.has_key("hi"):
        print somedict["hi"]

assert callable(foo)
foo({"hi":"there"})

オプションの -3 フラグ付きで実行するとこんな風に表示されます:

classics-MacBook-Pro:~ classic$ python -3 test.py
test.py:5: DeprecationWarning: callable() not supported in 3.x; use isinstance(x, collections.Callable)
  assert callable(foo)
test.py:2: DeprecationWarning: dict.has_key() not supported in 3.x; use the in operator
  if somedict.has_key("hi"):
there

ここに出ているすべての警告を直します。Python 2.3や2.4のような古いバージョンのPythonもサポートする必要がある場合には、実行系のバージョンやライブラリの検知するものを使わなければいけないでしょう。たとえば、Python 2.4にはcollections.Callableがありません。それ以外にもあります。さて、すべてのテストが -3 フラグ付きでパスしたとしましょう。

2.ライブラリ全体に2to3を実行して何が起きるかを見る

このステップはみんながよく知っている部分です。2to3ツールを実行して、特急券を手に入れましょう。たとえば、Makoで2to3ツールを実行したときはこんな感じでした:

classics-MacBook-Pro:mako classic$ 2to3 mako/ test/ -w

2to3は何をしているかをすべてstdoutにダンプします。 -w フラグはその場でファイルを上書きします。自分の場合は、ソースツリーをコピーして、オリジナルの代わりとなるPy2kのツリーとします。これがPython 2.xのソースをコミットするツリーとして残るわけです。大きなアプリケーションやライブラリでは、いくつか、もしかするとほとんどが2to3の処理を経て無傷ではいられない、なんていうのはよくあることです。
SQLAlchemyの場合は、普通の文字列型、Unicode型、Byte型の問題の他に、辞書型に対してiteritems()をitems()に、iteralues()をvalues()に名前を変更するところで問題がありました。--いくつかの独自の辞書型は壊れました。-3フラグで警告が一切無く、2to3ツールが動作しないコードを生成している場合、一般的に3つのアプローチでバージョン間互換性を達成することができます。以下、重要度の昇順で並べました。

2a. Py3kでは使えないイディオムをバージョン間互換になるようにする

一番簡単なのは、対象のコードが修正可能なら、Py3k向けに2to3ツールを実行した後、どちらのプラットフォームでも動作するように修正することです。一般的に多くのBytes/Unicodeの問題が起きます。例えば次のようなコードがあります:

hexlify(somestring)

...これはPy3kでは動作しません。hexlify()はBytesが必要なのです。これは次のように変更するのが適切でしょう:

hexlify(somestring.encode('utf-8'))

Makoでは、render()メソッドはエンコードされた文字列を返します。これはPy3kではBytesです。単体テストはこんな風にしています:

html_error = template.render()
assert "RuntimeError: test" in html_error

これを代わりにこんな感じに直しました:

html_error = template.render()
assert "RuntimeError: test" in str(html_error)
2b. 実行系バージョンフラグを使って使用時やライブラリの非互換性に対処する

SQLAlchemyはutilパッケージを持っていて、そこにはこんな感じのコードがあります:

import sys
py3k = sys.version_info >= (3, 0)
py3k_flag = getattr(sys, 'py3kwarning', False)
py26 = sys.version_info >= (2, 6)
jython = sys.platform.startswith('java')
win32 = sys.platform.startswith('win')

これは基本的には事前にフラグを取得して、異なるプラットフォームで固有の動作を選択できるようにしています。Py3k向け(あるいはJythonや古いバージョンのPython)の実行時の動作を切りたい時に、ライブラリの他の部分は from sqlalchemy.util import py3k とすればよいです。

if util.py3k:
    self.default_filters = ['str']
else:
    self.default_filters = ['unicode']

これを使って、特定の単体テストはサポート対象外とすることができます(skip_if()はNoseにおけるデコレータで、与えた式がTrueならSkipTestをraiseします):

@skip_if(lambda: util.py3k)
def test_quoting_non_unicode(self):
    # ...

先ほどいったcallable()に関する問題(Python 3.2で戻って来ますが)向けに、SQLAlchemyではcompat.pyモジュールで次ようなブロックがあります。このコードはcallable(), cmp(), reduce()を返します:

if py3k:
    def callable(fn):
        return hasattr(fn, '__call__')
    def cmp(a, b):
        return (a > b) - (a < b)

    from functools import reduce
else:
    callable = __builtin__.callable
    cmp = __builtin__.cmp
    reduce = __builtin__.reduce
2c. プリプロセッサを使う

"実行時フラグ"アプローチはおそらく90%くらいのPythonライブラリが行う必要があります。SQLAlchemyではより手荒な手段にでました。2to3ツールにプリプロセッサをくっつけたのです。この利点は互換性のない構文に対処できること、実行時の真偽値フラグによる遅延時間がどこか致命的になるのではないかという心配をしなくていいこと、そして個人的にはちょっと読みやすくなることです。特にクラス宣言では同じレベルのインデントを維持できます。
プリプロセッサはSQLAlchemyの配布の一部で、ここからダウンロードできます。いまのところは動作させるのにモンキーパッチとしています。プリプロセッサの使い方はフォーラムや講演で話してきましたが、いまだに他の人がこういったアプローチをとっているという話を他に聞いたことがありません。この方法をどうしたら良く出来るか、提案は大歓迎です。たとえばモンキーパッチなしにこういったことができる、普通の2to3"修正器"を取得する方法がある(仕事には使えませんでした--例を一つ挙げるとシステムがコメントを読み込まないというのがあります)とか、あるいはプリプロセッサに近い利点がある他の方法とかなんでも結構です。
例としてはIdentityMap辞書サブクラスがあります。ここに例として載せましたが、Python 2プラットフォームでイテレータを返すようにiteritems()を定義する必要があった一方で、Python 3ではitems()メソッドが必要でした:

class IdentityMap(dict):
    # ...

    def items(self):
    # Py2K
        return list(self.iteritems())

    def iteritems(self):
    # end Py2K
        return iter(self._get_items())

上のコードで、"# Py2K / # end Py2K"のコメントが取り上げられています。これが2to3ツールに渡されると、コードは次のようになります:

class IdentityMap(dict):
    # ...

    def items(self):
    # start Py2K
    #    return list(self.iteritems())
    #
    #def iteritems(self):
    # end Py2K
        return iter(self._get_items())

新しい構文が便利だという時もこの方法を使います。DBAPI例外を再度スローするときに、Python 3のfromキーワードを使って例外をまとめてチェーンさせるのがとてもいいですが、Python 2ではそれはできません:

# Py3K
#raise MyException(e) from e
# Py2K
raise MyException(e), None, sys.exc_info()[2]
# end Py2K

2to3ツールはwith_traceback()の呼び出しに変更しています。Python 2.6に対しても間違って行なってしまいます。(2.7で修正されました)fromキーワードは例外が"チェーン"に保存されるという点でwith_traceback()とは少々異なる意味を持っています。プリプロセッサを通すと、次のようなコードになります:

# start Py3K
raise MyException(e) from e
# end Py3K
# start Py2K
#raise MyException(e), None, sys.exc_info()[2]
# end Py2K

プリプロセッサが入力された文字列ストリームを修正したあと、それを2to3ツールに渡して残っているPython 2のイディオムがPython 3に変換されます。ツールは(幸運にも)すでにPython 3対応しているコードは無視します。

3. デュアルプラットフォーム配布をDistutils/Distributeで作る

さて、スクリプトを使ってソースをすべて動作するPython 3アプリケーションに変換できましたので、このスクリプトを2to3ディレクティブを使ってsetup.pyスクリプトと統合しましょう。説明書きが望まれ、その場合distutilsがこのフラグを受け付けます。しかしこれはDistributeがインストールされている場合でないと正しく動作しません。Porting to Python 3 ― A Guideがガイドラインとして役に立つでしょう。ここではArmin*2のコードサンプルをまるごと載せます:

import sys

from setuptools import setup

# if we are running on python 3, enable 2to3 and
# let it use the custom fixers from the custom_fixers
# package.
extra = {}
if sys.version_info >= (3, 0):
    extra.update(
        use_2to3=True,
        use_2to3_fixers=['custom_fixers']
    )


setup(
    name='Your Library',
    version='1.0',
    classifiers=[
        # make sure to use :: Python *and* :: Python :: 3 so
        # that pypi can list the package on the python 3 page
        'Programming Language :: Python',
        'Programming Language :: Python :: 3'
    ],
    packages=['yourlibrary'],
    # make sure to add custom_fixers to the MANIFEST.in
    include_package_data=True,
    **extra
)

SQLAlchemyでは、このアプローチをちょっと変更して、プリプロセッサがパッチされるようにしました:

extra = {}
if sys.version_info >= (3, 0):
    # monkeypatch our preprocessor
    # onto the 2to3 tool.
    from sa2to3 import refactor_string
    from lib2to3.refactor import RefactoringTool
    RefactoringTool.refactor_string = refactor_string

    extra.update(
        use_2to3=True,
    )

2to3フラグを使うことで、ソース配布がビルドされて、Python 2かPython 3インタプリタのどちらかにインストールされます。そしてもしPython 3だった場合、ソースがインストールされる前に2to3が実行されます。
いくつかのパッケージで2つの全く別のソースツリーを管理して、片方はPython 3版としているのを見ました。私はこういう選択をするパッケージが減ることを心より願います。なぜなら、ソースツリーを分けるということは、メンテナーに多くの仕事を強いるということ(あるいはPython 3版のリリースが遅くなる)、バグが増えること(単体テストが同じソースツリーに対して実行されるわけではないから)、そして最善の方法に思えないからです。結果的に、Python 3がデフォルトの開発プラットフォームになったとき、逆に3to2を使ってPython 2版を管理するおとになるのです。

4. Python :: 3 の分類辞を追加しよう!

たまにやり忘れるんですが、上のような例で、'Programming Language :: Python :: 3' を分類辞に追加することを覚えておきましょう!これはパッケージがPython 3で動作することを周知するもっとも主要な手段です。

setup(
    name='Your Library',
    version='1.0',
    classifiers=[
        # make sure to use :: Python *and* :: Python :: 3 so
        # that pypi can list the package on the python 3 page
        'Programming Language :: Python',
        'Programming Language :: Python :: 3'
    ],
    packages=['yourlibrary'],
    # make sure to add custom_fixers to the MANIFEST.in
    include_package_data=True,
    **extra
)

おまけ

PyCon US 2011でPython 3への移行について話したLennart Regebroの本です。実際の移行が始まる前に確認しておきたいですね。

Porting to Python 3: An In-Depth Guide

Porting to Python 3: An In-Depth Guide

*1:訳注:Pylons等のコミッター

*2:訳注:Jinja2, Werkzeug, Flask, Sphinxなどのコミッター