Web アプリケーションにおいては Python 3 への移行は簡単だという話

Python 2 / 3 両対応のライブラリをメンテナンスしている人にとって、 Python 3 の メリットは享受できずメンテナンスコストだけが上がるつらい状況がずっと続いています。

しかし、「Python 3 への移行が大変」というのは、Python 2 を捨てるのが大変、 Python 2/3 両対応の ライブラリをメンテナンスするのが面倒という意味です。 アプリケーションであれば、 Python 2 対応のプロジェクトを Python 3 に対応させると同時に Python 2 への対応を切れるのでずっと楽ですし、さらに新規プロジェクトを Python 3 で始めるのは もっと楽です。

特に Web 系では、 (Google App Engine などの例外はあるものの)自分で Python のバージョンを 選べることが多いし、主要なライブラリも Python 3 対応ができているので、十分コストパフォーマンスに 見合った選択肢になっています。

既存アプリを Python 3 に移植するのはともかく、新規アプリの開発を始めるのであれば、 まずは Python 3 でいけないか考えてみましょう。

Python 3 への移行が楽なこと、 Python 3 に移行するメリットを説明してみます。

Python 3 への移行が楽な理由

1. 移行と同時に Python 2 を切れる.

新規アプリではなく既存アプリの移植を行う場合や、既存アプリからある程度のコードベースを 持ってくる場合でも、 2to3 などを使った機械的な変換で対応作業の大部分を終わらせることができます。

2. 難しい部分はライブラリ・フレームワークがやってくれる

Python 3 対応で難しいのは、エンコーディング、入出力、 C 拡張モジュールなどの部分です。

Web アプリケーションを作る際、この部分はフレームワークやライブラリの中で処理されます。 Python 2 で Web アプリケーションを作る時から既にこの変換は行われていて、基本的に文字列は全て unicode で扱っていたはずです。 Python 3 になって何か追加でしないといけないことは無いです。

実際、殆どの Flask のサンプルアプリは Flask 0.10 が出た時に一文字も変更すること無く、そのままきれいな Python 3 製 Flask アプリのサンプルとして利用できました。

3. 主要なライブラリの移行が完了している

Python 2 を使っていても、メンテナンスが行われていないライブラリ、あまり実績の無いライブラリは 元から使わなかったでしょう。そして、メンテナンスがアクティブに行われていて広く使われている ライブラリのほとんどは Python 3 への対応ができています。

もちろん、いくつか例外があります。 gevent はまだ Python 3 に対応したバージョンを正式に リリースしていません。 gevent の場合、通常の Web アプリ部分と gevent を使う部分は 分けた方が良いので、 gevent の部分だけを Python 2 で動かせばよいでしょう。

Python 3 に積極的でない企業が管理しているライブラリも遅れています。 Google の oauth2client (汎用的にも使えるが、基本的に使われるのは Google API 用がメイン), google-apiclient-python などがそれです。 こういった Web API クライアントも、大抵だれかが Python 3 に対応した fork を用意してくれて いますし、最悪の場合 requests_oauthlib を経由して直接 API を叩けばよいでしょう。

(oauth2client は Python 3 対応ブランチを手伝っていて、現在 Google 社内レビュー待ちです。 google-apiclient-python はその後に Python 3 対応するつもりです)

他にも MySQL-python => mysqlclient, python-memcache => pymemcache など、実績あるけど メンテナンスが滞ってるライブラリから、 fork や別のメンテナンスされて実績もあるライブラリへの 乗り換えも必要になるかと思います。

乗り換えるライブラリがあまりに多いと Python 3 への移行が大変になるので、そういった既存プロジェクトの 移行は難しいかもしれません。しかし新規プロジェクトであれば最初から Python 3 に対応したライブラリを 選べばよいはずです。

Python 3 へ移行するメリット

1. UnicodeError の撲滅

Python 2 では、非 ASCII 文字が入ったバイト文字列が少しでもアプリケーションに入り込むと、 unicode と一緒にした瞬間に UnicodeError が発生します。このエラーは unicode と一緒に操作 した場所で例外が起こるので、そもそもそのバイト文字列がどんな経路で侵入したのかから調べないと いけません。

Python 2 でも非 ASCII 文字列を含むバイト文字列を使うことは推奨されませんが、もともとは普通の ことでした。そのため、標準ライブラリでも文字列を含むバイト文字列を使ってしまっているライブラリは たくさんあります。

Python 3 では文字列が全部 unicode なのでこういった問題がありません。

2. ログ、エラー表示を読みやすくできる

ログやエラー表示では、「オブジェクトの文字列化」が重要になります。

Python 2 ではデフォルトの文字列はバイト文字列であり、バイト文字列に非ASCII 文字を含めると UnicodeError が発生してしまう原因になるので、「エラーが起こってログに残そうとしたらログ用の コードがエラーはいて元のログ残せなかった」というケースが発生してしまいます。

そのため、 Python 2 ではオブジェクトの文字列化、特に __repr__() では、非 ASCII 文字を 出力しないことが推奨されます。

結果として、 Python 2 でログやエラー画面に何か日本語を含むオブジェクトを表示しようとした途端、 エスケープの嵐になります。

Python 2:

>>> print([u'こんにちは'])
[u'\u3053\u3093\u306b\u3061\u306f']

これが Python 3 になると、普通に日本語を表示できるようになります。

Python 3:

>>> print(['こんにちは'])
['こんにちは']

プログラマーがログを見る場合にも楽ですし、どうしても品質が1ランク落ちがちな社内運用者向けの Web アプリでは、運用者がエラー画面を理解できる確率が飛躍的に向上します。

エラーメッセージが理解できれば、「次バージョンでは直すけど、それまでこのエラーが出た時はこう対処して。ごめん!」 というだけで、エラーが起こるたびに呼び出されてサポートする手間が減ります。

3. Python 2 のバッドノウハウの多くが不要になり、教育が楽になる

上で書いたように、非 ASCII 文字を含むバイト文字列を避けるといったノウハウは不要になりました。 さらに、 codecs.open()io.open() で書き込みモードで開いたファイルでは ASCII だけの バイト文字列を書き込もうとするとエラーになるなど、 ASCII 文字だけでもバイト文字列がダメなケースが有ります。

他にも、 int と long が別の型になっていて、整数型かどうかのチェックに isinstance(x, (int, long)) と書かないといけないとか、継承しないときも class X(object): と書かないと10年前から時代遅れの旧スタイルクラスになるとか、 沢山の細かいガラスの破片が混ざっています。

Python 2 に慣れた人は、そういったガラスの破片を無意識に避けられるようになっているかもしれませんが、 新しい人にそれを教える意味があるのでしょうか?

Python 3 が Python 2 と非互換になっているのは、互換性を維持したままではガラスの破片を綺麗に掃除 できなかったからです。新しい人が Python 2 のガラスの破片の避け方を覚えるより、元からいる人が Python 3 で消えた破片を知るほうが建設的なはずです。

このブログに乗せているコードは引用を除き CC0 1.0 で提供します。