Django と Python 3 - #python_adv

Django-ja の方からきました。こんにちわ。さて、昨日の Ian 先生のブログにも書いてある通り、ついに Django にも本格的に Python 3 の足音が近づいてきました。ただし現在 alpha 版が公開されている 1.5 では "実験的" なサポートで、1.6 以降で正式にサポートする予定となっています。あくまでも "実験的" であり、プロダクションでの利用は "非推奨" となっています。Django コミュニティでは、この 1.5 でサポートをテストしてもらい、そのフィードバックを呼びかけています。なのでプロダクションでの利用は 1.6 まで待ちでしょうね。また Python 3 サポートと同時に、Django 1.4 では Python 2.4 がサポートから外れ、1.5 では Python 2.5 がサポートから外れます。これで 1.7 以降から django.utils 配下が軽くなっていくんでしょうかね (現在は 3 サポートのためにさらに増えてる)。

バージョン

下位互換

Python サポート

2.5

2.6

2.7

3.2

3.3

Django 1.4

> 1.2

-

-

Django 1.5

> 1.3

-

>= 2.6.5


>= 2.7.3


実験的


実験的

Django 1.6

> 1.4

-

>= 2.6.5


>= 2.7.3

Python 2.5 系を利用している場合は、1.6 のリリースまで (2013 後半ぐらい?) に Python 2.6.5 以上 (2.7.3 以上を強く推奨) への移行が必要です。1.5 は今月 (2012/12) 中にリリース予定とのことなので、今のうちに Python 3 へ移行方法を抑えときたいなと。以下 Django ドキュメントの翻訳作業がてら "Porting to Python 3" を元にご紹介。さすが Django のドキュメントだけあって Python 3 のお勉強にも良い感じでした。

Django は同じソースコードで Python 2 (>= 2.6.5)、3 (>= 3.2) 双方で動作するよう six という互換用レイヤーを利用しています。six による Python 3 対応については、Python ドキュメント "Python 2 から Python 3 への移植" に詳しく書いてあります。

思想

Python 2 と Python 3 の違い、変更については Python ドキュメント去年 (2012) のアドベントカレンダー等を参考にしてください。

Django は Python 2, 3 互換ですが、作成するアプリケーション自体は必ずしも互換性を持たせる必要はありません。プラガブルなアプリケーションの作成者は Django 同様の移植が推奨されています。Python >= 2.6 以降を対称にすると、より簡単に互換性を持たせることができます。

Django 1.5 より django.utils.six のような互換性ツールが導入されます。上位互換性のために Django 1.4.2 以降、互換性ツールへのエイリアスが用意されています。アプリケーションでこれら互換性ツールを利用する場合は Django 1.4.2 以降が必要です。

当然、互換性があるソースコードを書くのはコストがかかります。Django の開発者は、Python 3 互換の Python 2 コードを書くより、Python 2 と互換の Python 3 コードを書く方が価値が高いと判断しました。これはコードの寿命が延びるだけでなく、Python 3 の利点 (より正確な文字列の取り扱い等) の恩恵を受けることができます。Python 2 は下位互換でサポートされます。

Django が提供する移植用のツールはこれらの思想に基づいています。

移植のテクニック

>>> ユニコードリテラル

このステップは

  • Python モジュールの先頭に from __future__ import unicode_literals を追加 - すべてのモジュールにそれぞれ追加するのが最適です
  • ユニコード文字列の接頭辞 u を削除する
  • バイト文字列の接頭辞 b を追加

体系的にこの変更を実施すると下位互換を保証することができます。しかし、Django はユニコードのインターフェスを公開して以降、バイト文字列は必要とされていません。Python 3 はバイナリデータ、バイト指向インターフェスを除いて原則として文字列はユニコードを利用します。Python 2 では、アスキーデータのみの場合に限り、バイト文字列とユニコード文字列が変換可能です。可能な限り接頭辞 b を避け、ユニコード文字列を利用するようにします。

>>> 文字列処理

Python 2 の unicode 型は Python 3 の strstrbytes に置き換えられ、basestring は無くなりました。six はこれらの変更に対応するためのツールを提供しています。Django にも django.utils.encodingdjango.utils.safestring モジュールにより、いくつかの文字列に関するクラスと関数を提供しています。そのクラス名、関数名には Python 2 と Python 3 で意味が異なる str 、Python 3 にはない unicode が利用されていました。曖昧さの回避、及び明確化のため bytestext と名前を変更しました。

django.utils.encoding の名前は以下のように変更となっています。

smart_str smart_bytes
smart_unicode smart_text
force_unicode force_text

下位互換のため、旧関数も Python 2 上で動作します。Python 3 上では、smart_strsmart_text のエイリアスとなっています。また、上位互換のため、新関数は Django 1.4.2 以降で動作します。

django.utils.safestring は主に、mark_safe()mark_for_escaping() 関数を通して利用されており、それに変わりはありません。内部で直接呼び出している場合は以下のように変更となります。

EscapeString EscapeBytes
EscapeUnicode EscapeText
SafeString SafeBytes
SafeUnicode SafeText

下位互換のため、旧関数も Python 2 上で動作します。Python 3 上では、EscapeStringSafeString はそれぞれ EscapeTextSafeText のエイリアスとなっています。また、上位互換のため、新クラスは Django 1.4.2 以降で動作します。

>>> __str__()__unicode__() メソッド

Python 2 では、オブジェクトは __str__()__unicode__() というメソッドを指定します。これらのメソッドはそれぞれ str (bytes) と unicode (text) を返します。

print 構文やビルトイン関数の str()__str__() をコールし、オブジェクトを可読可能にします。ビルトイン関数の unicode()__unicode__() があればコールし、ない場合は __str__() の結果をシステムエンコーディングでデコードした結果を返します。逆に Django の Model を継承したクラスは __str__()__unicode__() を UTF-8 でエンコーディングした結果を自動的に返します。

Python 3 には、 str を返す __str__() しかありません。( __bytes__() を定義することもできますが、bytes の取扱いが非常に難しいため Django アプリケーションではほとんどこのメソッドを利用しません)

Django では Python 2 と 3 で __str__()__unicode__() メソッドを定義できるシンプルな方法が提供されています: 文字列を返す メソッドを定義し、python_2_unicode_compatible() デコレータを適用します。

Python 3 上では、このデコレータは何も処理されません。Python 2 では、適切な __str__()__unicode__() メソッドを定義します (オリジナルの __str__() メソッドを置き換えます)。たとえば、以下のようになります:

from __future__ import unicode_literals
from django.utils.encoding import python_2_unicode_compatible

@python_2_unicode_compatible
class MyClass(object):
    def __str__(self):
        return "Instance of my class"

このテクニックは Django の移植における思想が反映されています。

上位互換のため、このデコレータは Django 1.4.2 以降で動作します。

最後に、__repr__() は Python のすべてのバージョンで str を返さなければならないので、注意が必要です。

>>> dictdict ライクなクラス

Python 2 では dict.keys()dict.items()dict.values()list を返しますが、Python 3 ではイテレータを返します。django.utils.datastructures に定義されている QueryDictdict ライクなクラスは Python 3 と同様の動作をします。

six はこの変更と互換の iterkeys()iteritems()itervalues() 関数を提供しています。Django にはそれに加え MultiValueDict とサブクラス用に iterlists() 関数が追加されています。

>>> HttpRequest と HttpResponse オブジェクト

PEP 3333 (和訳) への対応:

  • ヘッダーは常に str オブジェクト
  • インプットとアウトプットオブジェクトは常に bytes オブジェクト

具体的には、HttpResponse.contentbytes オブジェクトが含まれています。テストの際に str と比較すると問題が発生するかもしれません。この解決策は assertContains()assertNotContains() を利用します。これらのメソッドはレスポンスとユニコード文字列を引数にとることができます。

コーディングガイドライン

このガイドラインは Django のソースコードに適用されます。同様の移植手法を選択するサードパーティアプリケーションにも推奨されます。

>>> シンタックス要件

ユニコード

Python 3 においては、すべての文字列はデフォルトでユニコードとみなされます。Python 2 の unicode 型は Python 3 では strstrbytes となります。Python 3.2 ではシンタックスエラーとなるため、ユニコード文字列の接頭辞 u は使用しません。バイト文字列には接頭辞 b をつける必要があります。Python 2 でも同様の振る舞いを可能とするため __future__ から unicode_literals をインポートする必要があります。

from __future__ import unicode_literals

my_string = "This is an unicode literal"
my_bytestring = b"This is a bytestring"

もし Python 2 においてバイト文字列リテラルが必要な場合や、Python 3 においてユニコード文字列リテラルが必要な場合は、ビルトインの str() を用います:

str('my string')

Python 3 では、strbytes は自動的に変換されず、codecs モジュールはより厳格になりました。str.decode() は常に bytes を返し、bytes.decode() は常に str を返します。

その結果、時には以下のようなパターンが必要となります:

value = value.encode('ascii', 'ignore').decode('ascii')

バイト文字列へのインデックスには注意が必要です。

例外

例外を受ける際は、以下のキーワードを追加います:

try:
    ...
except MyException as exc:
    ...

以下の旧シンタックスは Python 3 で削除されました:

try:
    ...
except MyException, exc:    # Don't do that!
    ...

トレースバックの変更等で、再度例外を投げる場合のシンタックスは、 six.reraise() を使用します。

>>> 特殊メソッド

Python 3 で変更された特殊メソッドを使用するために、以下のパターンを使用します:

イテレータ

class MyIterator(six.Iterator):
    def __iter__(self):
        return self             # implement some logic here

    def __next__(self):
        raise StopIteration     # implement some logic here

真偽値

class MyBoolean(object):

    def __bool__(self):
        return True             # implement some logic here

    def __nonzero__(self):      # Python 2 compatibility
        return type(self).__bool__(self)

除算

class MyDivisible(object):

    def __truediv__(self, other):
        return self / other     # implement some logic here

    def __div__(self, other):   # Python 2 compatibility
        return type(self).__truediv__(self, other)

    def __itruediv__(self, other):
        return self // other    # implement some logic here

    def __idiv__(self, other):  # Python 2 compatibility
        return type(self).__itruediv__(self, other)

>>> six で互換性のあるコードを書く

six は、Python 2 と Python 3 を単一のコードベースでサポートするための標準的な互換性ライブラリです。まず、six のドキュメントを読んでください。

six は、バージョン 1.4.2 以降の Django にバンドルされています。それは django.utils.six でインポートできます。互換性があるコードを書くための最も基本的な変更は以下の通りです。

文字列処理

basestringunicode 型は Python 3 から削除され、 str の意味が変更されました。これらの型のテストには以下の方法を使用します:

isinstance(myvalue, six.string_types)       # replacement for basestring
isinstance(myvalue, six.text_type)          # replacement for unicode
isinstance(myvalue, bytes)                  # replacement for str

Python >= 2.6 は bytesstr のエイリアスとして提供されているため six.binary_type は必要ありません。

長整数 (long)

Python 3 に long 型はありません。1L はシンタックスエラーとなります。six.integer_types により、値が整数型か長整数型か調べることができます。

isinstance(myvalue, six.integer_types)      # replacement for (int, long)

xrange

xrange を使用する際は、 six.moves.xrange() をインポートします。

モジュールの移動

Python 3 ではいくつかのモジュール名が変更となりました。django.utils.six.moves モジュールにより、インポートに互換性を持たせることができます。

urlliburllib2 は一から作り直されており、django.utils.six.moves では処理されません。Django では、以下のように明示的に両方からインポートを試みます:

try:
    from urllib.parse import urlparse, urlunparse
except ImportError:     # Python 2
    from urlparse import urlparse, urlunparse

PY3

Python 2 と Python 3 で異なるコードが必要な場合は six.PY3 で確認します:

if six.PY3:
    # do stuff Python 3-wise
else:
    # do stuff Python 2-wise

これは six で解決できない場合の最終手段です。

>>> six のカスタマイズ

Django にバンドルされている six には、いくつかの関数が追加されています:

iterlists (MultiValueDict)

MultiValueDict の値をリストとするイテレータを返します。これは Python 2 では iterlists() ですが、Python 3 では lists に置き換わっています。

assertRaisesRegex (testcase, *args, **kwargs)

これは Python 2 の testcase.assertRaisesRegexp を Python 3 の testcase.assertRaisesRegex に置き換えます。まだ現行の Python 3 には assertRaisesRegexp はありますが、警告がでます。

Django バージョンの six はデフォルトに加え、 thread_threaddummy_thread_dummy_thread に置き換えます。

--

良くも悪くも Django らしい Python 3 対応ですね。というか WAF についてほとんど触れてない気がします。ごめんなさい。これを読んで Django の思想やドキュメントの素敵さが紹介できればと思います。Django-ja ではこの素敵なドキュメントの翻訳にご協力してくださる方を絶賛募集中なのでぜひ!Django ネタが続きましたが、明日は @methane 先生による flask のネタです。

コメント

このブログの人気の投稿

Python から Win32 API 経由で印刷する

Disqus のスケール - Django 編

#PySpa アドベント (23 日目)