最近のPython-dev(2017-09)
バックナンバー: 8月号 | 6月号 | 5月号 | 4月号 | 3月号 | 2月号 | 1月号
今月は Sprint がありました。去年の Sprint はベータ版直前だったのでたくさんの実装が入りましたが、次の Python 3.7 のベータは来年のはじめなので、今回は実装よりも提案(PEP)が多めです。とても全部は紹介しきれない(そもそも一部を除いて議論を追えていない)ので、今月からは提案については受理されたものや受理間近のものだけ紹介していきます。
namedtuple 生成の高速化
bpo-28638: Optimize namedtuple() creation time by minimizing use of exec()
namedtuple という、タプルの要素に整数の添字ではなく属性名でアクセスできるようにするデータ構造があります。 例えば次のようにして使われます。
_CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"])
これは動的にクラスを作るのですが、そのためにクラス宣言の Python コードを文字列置換で生成し、 eval() することでクラスオブジェクトを生成していました。このメタプログラミングの方式は、リフレクション的な機能を使ってクラスを生成するよりも、何をやっているのかが判りやすいというメリットがあります。
しかし、通常のPythonのコードはバイトコードにコンパイルされて pyc ファイルにキャッシュされるのに対し、この方式ではクラスを生成するコードは都度コンパイルされてしまいます。そのため、 namedtuple をたくさん使っているモジュールの読み込みは遅くなります。
そこで、より一般的な type()
関数を使う方式で namedtuple が書き直されました。とはいえ、生成されたクラスのインスタンス生成速度に影響する __new__
メソッドだけは、性能を落とさずに完全な後方互換性を維持できなかったので、1関数だけのごく短いコードに対するevalはまだ残っています。それでも、CPythonでもPyPyでも数倍速くなっているので、namedtupleをたくさん利用するライブラリをインポートする時間が短縮されます。
その他、複数の namedtuple で作ったクラス間で共有できる部分を共有するなどの最適化が盛り込まれ、(インスタンスではなくて)生成されたクラスオブジェクトのメモリ使用量も削減できています。
なお、従来は生成されたクラスに ._source
という属性があり、 eval 対象になったクラス定義のソースコードが入っていたのですが、今回の改良でなくなりました。
OrderedDict のコンパクト化
去年私が dict をコンパクト、かつ挿入順を維持する実装をしたのですが、まだ collections モジュールの OrderedDict は dict と別にキーの双方向リンクリストを持っていて、 dict の倍のメモリを利用します。
OrderedDict の典型的な用途は単に json などで順序を維持したいというものですが、その用途なら dict を使ったほうが、メモリ使用量は半分になり、構築も列挙も高速です。しかし、 dict の順序は実装依存であり、CPythonとPyPyは順序を維持するものの、それに依存するのはお行儀が悪いです。
そこで、OrderedDictをdictの構造をそのまま利用するように書き換えて見ました。メリットとしては典型的な操作の性能があれこれ上がっているのと、メモリ使用量が1/2に、そしてソースコードも1000行以上削ることができました。
しかし、デメリットとして OrderedDict と dict が密結合する(今は分離されているソースコードを1つにマージしてしまう)ことと、 OrderedDict だけに存在する move_to_end()
というメソッドの速度が数割落ちています。他にも、要素の移動や追加削除の平均計算量は O(1) のままだけれども、最悪計算量が O(n) になってしまうようなパターンが増えている可能性もあります。
この部分のエキスパートである Raymond Hettinger さんは特に大きな書き換えに厳しい人なので、説得するには実装の磨き上げとより詳しい検証が必要です。多分 ISUCON 後になるけれど、 Python 3.7 に間に合わせたい。
PEP 539 v3: A new C API for Thread-Local Storage in CPython
Yamamoto Masayuki さんが活動されていた、スレッドローカルストレージを利用するCレベルの新APIの提案が受理されました。
旧APIはかなり古くからあるのですが、TLS key として int 型を使っていて、LinuxやメジャーなUnix、Windowsでは問題ないもののPOSIXには準拠していませんでした。そのためCygwinなどで問題が有ったらしいです。
PEP 557: Data Classes
主にデータの入れ物となることを目的としたクラスをより手軽に作れるようにするためのAPIが提案されています。
@dataclass class InventoryItem: name: str unit_price: float quantity_on_hand: int = 0 def total_cost(self) -> float: return self.unit_price * self.quantity_on_hand
のように宣言すると、
def __init__(self, name: str, unit_price: float, quantity_on_hand: int = 0) -> None: self.name = name self.unit_price = unit_price self.quantity_on_hand = quantity_on_hand def __repr__(self): return f'InventoryItem(name={self.name!r},unit_price={self.unit_price!r},quantity_on_hand={self.quantity_on_hand!r})'
のようなメソッドが自動で生成されます。
namedtuple と目的が被っているものの、 namedtuple はタプルであるために一部の用途には向かない事があります。例えば immutable だとか、普通のタプルと比較可能であるなどです。
Data Class はタプルではなく、そのためより柔軟です。同一性でなく同値性による比較可能にするかどうかなどが選べます。構文も Python 3.6 からの新しいスタイルを活用したもので、例えば namedtuple だと独自のメソッドを追加するにはもう1段継承したクラスを作るなどの面倒があったのですが、こちらは上の例の total_cost()
メソッドのように普通に書くことができます。
このPEPは受理一歩手前で、あとはもう名前を決めるだけです。個人的には Record がいいな。
PEP 552 -- Deterministic pycs
pyc ファイルには py ファイルのタイムスタンプが格納されていて、 py ファイルが更新されると自動的に pyc ファイルも作り直されるようになっています。
このためにpyファイルが同じでもpycはバイナリレベルでは一致しません。これが最近の ビルド再現性(Reproducible Builds) というプラクティスには都合がよくありません。 (Debian/Ubuntu では pyc はインストール後にビルドされますが、 pyc を一緒にパッケージに含めているディストリビューションもあります)
また、Bazelというビルドシステムでもこれは都合が悪いらしいです。
この PEP ではヘッダーの形式を追加し、従来通りのタイムスタンプ+自動ビルド方式の他に、pyファイルのハッシュ値を持つ事ができるようになります。この場合、 pyc を読み込むときに py ファイルのハッシュ値を計算するコストが気になるところですが、新しいヘッダはpy ファイルのハッシュをチェックして自動リビルドするか無条件に pyc を使うかを選択するフラグを持っています。
pyファイルをチェックしない方を選択した場合も、 import 時にチェックしないだけで、コマンドを使って明示的にチェックしたりリビルドすることはできます。なので root しか書き込めないディレクトリに Python やライブラリをインストールする場合など、ユーザーがうっかり py ファイルを変更する危険が無い場合は問題ないでしょう。
このPEPはAccept直前(他に意見がなかったらAcceptするよとGuidoが宣言中)です。なお、このPEPはファイルフォーマットレベルでの問題を修正するだけで、それ以外にも pyc ファイルが一定にならない実装上の理由は幾つかあります。しかしこのPEPがAcceptされたということは、 deterministic pyc をサポートするという方向性が決まったことでもあるので、実装面の課題も今後修正されていくと思います。
起動高速化
インタプリタの起動高速化は難しい問題なのですが、実際のアプリケーションの起動ではその何十、何百倍の時間が、ライブラリのロードに消費されています。
あるモジュールを import しても、そのモジュールの全部の機能を使うわけではありません。そのモジュールの中で、あまり使用頻度が高くないと思われる関数でだけ使われる依存関係は、モジュールの先頭ではなくその関数で import することで、アプリケーションの起動時間を減らすことができるかもしれません。
一方で、それは PEP 8 違反(明確な理由やメリットがあったら違反しても良いです)ですし、その関数の実行時間は若干遅くなってしまいます。だから、利用頻度が少なく、かつその利用頻度に比べて import が重いモジュールに絞って書き換えが進んでいます。
argparse が直接、間接的に import しているモジュールが減らされました。(マージ済み) https://github.com/python/cpython/pull/1269
functools の singledispatch でしか使われないモジュールを singledispatch 内に移動します。 (accept待ち) https://github.com/python/cpython/pull/3757
uuid モジュールが、 uuid1 のために libuuid や UuidCreateSequential をロードする処理が重いのでそれを遅延する提案。(これからレビューします)
https://github.com/python/cpython/pull/3684
また、重い import を見つけるために私がローカルで使っていた import に掛かる時間を表示するパッチを、だれでも(特にライブラリやフレームワークの作者が)簡単に使えるように -X importtime
オプションとして提案しています。今はその出力フォーマットについて自転車置き場の議論をしています。
https://github.com/python/cpython/pull/3490
余談ですが、昔幾つかのサードパーティーライブラリの import 時間を調べて居た所、 Jinja2 の import がすごく遅いことに気づきました。
実は今年のはじめにメモリ使用量を解析していたところ、 Jinja2 は Python 3 ではユニコードのシンボルも(テンプレートエンジン内の変数名として)使えるようにしようと、シンボル名として有効な名前を表現する正規表現をかなり強引な方法で作っていたのを見つけて報告していたのですが、その正規表現のコンパイルが import 時に実行されていて遅かったのです。
すでに Jinja2 の開発ブランチではずっと良い実装に切り替わっているので、次の Jinja2 のリリースを楽しみにしています。