Python 3 への移行を見据えた Python 2.7 コードの書き方
前提
はい。地獄ですね。
どうしてそんなことを?と思われることでしょうから前提を説明しますね。
まず、私が所属している業界は 3DCG がらみのところで、DCC ツール (Digital Contents Create Tools) やゲームエンジンを用いて何らかのプロダクトを開発していきます。世の DCC ツールの多くは、標準機能にない機能をプラグインで補えるようになっており、多くの場合で記述言語として Python が採用されています(なんで?)。Python 製プラグインは、DCC ツール組み込みの Python で実行されます。
VFX Reference Platform とかいうやつ
この DCC ツール組み込み Python なんですが、VFX Reference Platform が毎年発表する各開発環境の推奨バージョンに従っているんですね(Blender など、英断をした一部の DCC ツールを除く)。Reference Platform の CY2019 を見てみると、 https://scrapbox.io/files/656ab827dc1a6800240793df.png
2.7.9 - 2.7.latest (Python 3 tech preview release)
とありますね。つまり、2019年までにリリースされた多くの DCC ツールに組み込まれている Python が Python 2 ということです。
大人の事情
そんな昔のもん誰も使っとらんやろと思うかもしれませんが、ゲームのような開発現場では、DCC ツールのバージョン依存の問題発生を防止するために、プロジェクトが開始されたタイミングで使用ツールのバージョンを固定するということが珍しくありません。また、AAA タイトルなど、開発には長い年月を要するものも近年では珍しくありません。
あとはあれですね、Ax 社が 遅々として移行を進めない ということもありますね。2024年4月末まで Python 2.7 サポートって、そんな伸ばす必要ないでしょとしか思えないのですが、何かあるんでしょう、大人の事情が。頼みますよ、Ax 社。 終わりました。よかったですね。
せめてものあがき
これらの事から、2019年より前に開始されたプロジェクトなどでは、未だに Python 2.7 が普通に使われているということです。そのようなプロジェクトで新規に Python 2.7 コードを書くということもあるでしょう。ですので、せめて Python 3 に移行した際に既存のコードを流用できるように書いておこうというのがこの記事の趣旨です。
本題
前置きが大分長くなりましたが、本題に入ります。
Python 2.7 と Python 3 の互換を保つには、基本的には __future__ と six を使っておけば大体なんとかなります。Python 2.7 以前の面倒は見たくないので、そこの互換性は考慮していません。 __future__
ドキュメントの mandatory in 3.0 のものをインポートしておけばよいです。ただし、unicode_literals は他のライブラリとのやり取りで混乱が生まれそうなため、私は外していて、それ以外の3つをファイルの先頭でインポートするようにしています。
こんな感じです。
code:よくあるPython2互換コードの冒頭.py
# coding: utf-8
from __future__ import absolute_import, division, print_function
...
six
six は、Python 2 と Python 3 の差異を吸収して Python 3 への移行をスムーズにするための便利なライブラリです。
例えば、Python 2 と Python 3 では下記の表のようにビルトイン型が違います。
table:表1__Python2とPython3の型の違い
型 Python 2 Python 3 six
バイト列(ネイティブな文字列) str bytes six.binary_type
ユニコード文字列 unicode str six.text_type
整数型 int int six.integer_types
長整数型 long int six.integer_types
この差異を隠蔽するための型が six に用意されています(表1右端の列)。これは isinstance で型をチェックするときや、docstring に型を書くときなどに便利です。
また、サードパーティーライブラリの関数・メソッドを使用した際に、Python 2 と Python 3 で要求される引数の型が違うということがあります。よくあるのが Python 2 でも Python 3 でも str を引数に要求されるケースです。私の場合、自前のコード内では文字列を six.text_type で統一しておき、外部ライブラリを使用する直前で適宜変換するという流れにすることで大部分のコードを共通化しています。しかし、外部ライブラリを使用する直前での変換処理では、Python のバージョンごとの分岐が生じてしまいます。自前で書きたくないですよね。そこで便利なのが six.ensure_str です。これを使えば six が Python ランタイムのバージョンを 確認し、それぞれのバージョンの str に変換してくれます。これにより、自前のコードがほぼ完全に共通化されます。他の six.ensure_<type> も同様です。ありがたいですね。
あとは、Python 2 と Python 3 の標準ライブラリの差異の吸収ですね。このへん に書いてありますが、いろいろ名前が変わっていますね。名前が変わったものを Python 3 のネーミングで使えるようにするのが six.moves です。素晴らしいですね。これにより、標準ライブラリの書き方も共通化できます。嬉しいですね。 このように素晴らしい six ですが、注意点が一つあります。six 自体が悪いわけではないのですが…。それは、DCC ツールなどの Python の site-packages にあらかじめ用意された six を使用するケースです。この six が最新バージョンではなく、six.ensure_str などの比較的新しめの機能が使えずハマる場合があります。Ax 社、お前の事やぞ!ですので、はなから何も信用せずに自分で最新版の six を用意するのが安全かと思います。MIT ライセンスで1ファイルの小さいライブラリですので、ハマるぐらいなら常に最新版を同梱しましょう。
他、気を付ける点
型アノテーション
使えません。ですが、最近の IDE はえらいので、型アノテーション以外の型ヒントでも型を認識してくれる場合が多いので割となんとかなります。型アノテーション以外の型ヒントとは、Type comments と Docstring の二つです。Type comments については PEP484 の ここ に説明があります。要はコメントとして型を書くやつです。Docstring については型の書き方が何種類かあるようですが、reStructuredText のフォーマットがスタンダードと言えるでしょう。下記のような感じに書きます。 code:re_structured_text_format.py
def hoge(fuga):
u"""関数の説明をこのへんに書く
:param fuga: 自然言語による引数の説明
:return: 自然言語による返り値の説明
:type fuga: ここに型を書く
:rtype: ここに返り値の型を書く
:raise <何らかのException>: 自然言語による説明
"""
<関数の中身>
コードの文字コード
Python 2 互換で書く場合、Python コードをどの文字コードで読むかを明示しておいた方が事故らなくていいです。Python 3 の場合は暗黙に UTF-8 の文字列としてコードが扱われますが、Python 2 はそうではないからです。
ファイルの先頭に
code:python2_style.py
# coding: utf-8
と常に付ける癖を付けておきましょう。
クラス
Python 2.2 から導入された新スタイルのクラスを明示的に使用すると、継承時の事故を防げるでしょう。
新スタイルのクラスは、継承ツリーの祖先に object を持つようなクラスです(普通はルートに持つ)。
Python 2 では新スタイルのクラスとするには object を親クラスとして明示しますが、Python 3 では親クラスを省略したら暗黙的に object の継承クラス(新スタイル)となるようになっています。
コードにするとこんな具合です。ややこしいですね。
code:コード1__Pythonのクラス.py
# Python 2 old style class
class Hoge:
...
# Python 2 new style class
class Hoge(object):
...
# Python 3 default class (= Python 2 new style class)
class Hoge:
...
ですので、Python 2, 3 互換を考えると親クラスの object を省略せずに明示する書き方を取るのがよいです。
リストがイテレータに変更されたもの
これはよくあります。for 文で回す分には問題になることはないのですが、Python 2 で list として扱っていたものが Python 3 で Iterator に変更されていて動かないというケースが時々あります。例えば zip 関数なんかがそうです。
code:コード2__Python2のzipの型.py
Type "help", "copyright", "credits" or "license" for more information.
<type 'list'>
list 型ですね。
code:コード3__Python3のzipの型.py
Type "help", "copyright", "credits" or "license" for more information.
<class 'zip'>
Iterable な zip クラスになってますね。気を付けましょう。
おわり
いかがでしたか?
この記事が誰かの役に立つ時代が可及的速やかに終わることを願っております。
明日は meloviliju さんの「用語集更新する」です。