言語としての一貫性を重視したPython 3の進化よりPythonicなPythonを目指して(後編)(1/2 ページ)

Python 3.0では、Python 2で書かれたスクリプトが動かなくなるような実装が行われた。なぜ、後方互換性を崩してまで大きな仕様変更を行ったのか。それは、PythonがよりPythonらしくあるためだ。

» 2009年02月06日 00時00分 公開
[柴田淳ウエブコア株式会社]

 前編「Python 3が後方互換性を捨てても求めたもの」では、後方互換性を犠牲にしてでも、よりPythonらしくあるために仕様を変更したことに触れながら、いくつかの機能変更を見てきました。

 それは、「誰もが正しいと考える、たった1つの方法をできる限り採用する(There should be one-and preferably only one-obvious way to do it)」というPythonの設計思想を、より高次元で実現するために必要だったからです。

 引き続き、後編ではPython 3.0で変更された仕様や新たに追加された機能を追いながら、Pythonという言語が目指す進化の方向性を探ってみましょう。

int型の統合、数値の扱いの変更

 Python 2では、文字列型だけでなく、整数型にも2つの種類がありました。C言語のlong型に依存したint型です。もう1つは、メモリの許す限り大きな数値を扱えるlong型です。

 もともと精度の低いint型がPythonに作られ、より大きな数値を扱いたいという要求からlong型が追加されました。int型とlong型をリテラルで書き分けられるように、数字の末尾に「L」を付けるというlong型のためのリテラルが用意されました。

 整数のための型が2種類あることも、古くからPythonの言語実装の一貫性を崩す仕様として議論されてきました。Pythonでは、仕様変更や機能追加に先だってPEPと呼ばれる技術文書が公開されます。PEP 237にint型とlong型を統合し、long型のみを整数型として採用するという提案がなされています。

 一見簡単そうに見える整数型の統合ですが、細かく見るといろいろな問題を引き起こす可能性をはらんでいます。PEPには、後方互換性に関する考察が書かれています。以下はその一部です。

  • 統合によってlong型のリテラルが廃止され、プログラムの修正が要求される
  • ビットシフト演算で、int型はビット落ちを起こすが、long型はビット落ちを起こさない
  • int型の0xffffffffは-1相当になるが、long型として解釈すると2の32乗-1となる

 前編で紹介したように、Python 3.0では後方互換性を崩す仕様変更が許されました。そこで、2種類の整数型が統合され、long型相当の数値型に統一されました。PEP 237が提出されたのは2001年ですから、7年以上も統合が見送られていたことになります。

 また、Python 2までは、int型同士の割り算は必ずint型を返していました。結果が小数点を含む数値となった場合は、内輪で一番近い整数を結果としていました。「1/2」の結果は「0」になります。

 Python 3.0では、int型同士の割り算は必ずfloat型の数値を返します。なお、従来通りint型を得たい場合には「//」という演算子を使います。

 数値型関連の機能追加では、ほかに2進数を扱う「0b1010」というリテラル表記が追加されています。Python 2までは8進数を扱うリテラルが「0666」だったのが、英語の8進数を意味する「Octal notation」からとった「O(オー)」を使った「0o666」というリテラルに変更されています。

 16進数が「0xABCD」のような表記ですので、数値のリテラル表記も、拡張をしながら一貫性を持ったものに変更されていることが分かります。

イテレータとview

 Python 3.0では、これまでリストを返していたメソッドや組み込み関数がイテレータやview(ビュー)と呼ばれる軽量のイテレータ風オブジェクトを返すようになりました。

 関連した仕様変更でもっとも目立つのは、組み込み関数xrange()とrange()の統合かもしれません。xrange()もrange()も、for文などに添えて使い、順番に増減するシンプルな数値のリストを生成するために利用されます。

 Python 2までのrange()はリストを生成します。このため、何十万回も繰り返し処理を行うような場合はメモリ効率が悪くなってしまいます。range()を呼び出すと、繰り返しの回数に必要な何十万もの要素を持ったリストを事前にメモリ上に用意するからです。

 そのため、このような場合はxrange()を使いました。xrange()はイテレータ風のオブジェクトを生成します。数を順番に生成するだけなので、すべての要素をメモリ上に置かないため、メモリの使用効率上利点があるのです。

 Python 3.0では、range()関数がイテレータ風のオブジェクトを返すようになり、xrange()が不要になったため廃止になりました。状況によって使い分ける必要があった2つの関数が1つに統合されて、より一貫性が増したわけです。

 Python 2.2でイテレータが追加されたときから、range()とxrange()統合の議論はあったのですが、Python 3.0でやっと統合されたことになります。

 range()だけでなく、map()やfilter()、zip()といったリストを返していた関数も、イテレータを返すようになります。map()関数では、引数として与えたシーケンスに対して、一度に処理をするのではなく、逐次処理をするようになるわけです。

 辞書のような組み込み型のメソッドにもリストを返すものがありました。辞書オブジェクトのキー一覧を返すkeys()、値一覧を返すvalues()、キーと値のペアを一覧で返すitems()といったメソッドがその例です。

 これらのメソッドは、リストより軽量で粒度の低いイテレータ風のviewと呼ばれるオブジェクトを返すようになりました。

 Python 3で辞書オブジェクトのkeys()を呼び出すと、dict_keysという名前のオブジェクトが返ってきます。これがviewと呼ばれるタイプのオブジェクトです。イテレータと同じ働きをして、イテレーションのたびに次のオブジェクトを返します。自分自身では値を保持せず、呼び出し元の辞書オブジェクトにリファレンスを張るような形でキー一覧を返します。

 Python 3.0で、メソッドの返すviewを変数に代入してみましょう。

$ python3.0
Python 3.0 (r30:67503, Dec5 2008, 09:49:50)
[GCC 4.0.1 (Apple Inc. build 5484)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
 >>> d=dict(a=1, b=2, c=3)    # 辞書オブジェクトを作る
 >>> ks = d.keys()
 >>> ks                       # viewオブジェクトを表示して確認
<dict_keys object at 0x33d9d0>
 >>> list(ks)                 # キー一覧をリストに変換
['a', 'c', 'b']
 >>> d.update(d=5)            # キーを追加
 >>> list(ks)                 # キー一覧をリストに変換
['a', 'c', 'b', 'd']

 このように、viewオブジェクトは辞書本体とのリファレンスを保っているだけなので、辞書本体にキーが追加されるとviewの結果も更新されます。

 keys()が返すキーの一覧は、辞書オブジェクト自体が内部に持っています。Python 2までは、辞書オブジェクトがすでに持っている情報を、わざわざリストに変換していたわけです。

 Pythonのリストは、メモリ領域伸張のコストを下げるため、あらかじめ要素数以上のメモリ空間を占有します。表面で見える以上に粒度の大きなオブジェクトなのです。リストオブジェクトの生成にオーバーヘッドが大きいため、もっと粒度の低いオブジェクトを使うべきという議論がありました。そのため、今回のような仕様変更が行われたのです。

 リストを返していたメソッドや組み込み関数が、イテレータやviewを返すようになったため、リストであることを期待したコードは修正が必要になります。例えば、関数の戻り値に直接インデックスを指定するようなコードは、動かなくなってしまいますので注意が必要です。

       1|2 次のページへ

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。