Python 3.6 の(個人的に)注目の変更点

Python 3.6b1 がリリースされましたね。(フライング)

beta1 ということで、 3.6 に向けた新機能の追加は (provisional package を除いて) 終了です。ただし、仕様が確定したと言うわけではなくて、beta版に対するフィードバックを元に新機能を修正したり、最悪 revert して 3.7 に持ち越しにされる可能性もあります。

なお、 3.6b1 が出る前の1週間が core dev sprint があり、そこでめちゃくちゃ大量に大きめの変更が入りました。なので、常用環境には全くオススメできませんが、OSS開発者だったら .travis.ymlpython: "nightly" を追加してリグレッションの発見に貢献したり(←これめっちゃ有り難いです)、それ以外の人も 3.6 を試してみて早めにフィードバックをしてもらえると、年末の 3.6 がより完成度が高いもになると思います。

では行きましょう

dict が省メモリかつ挿入順を保持するようになった

僕がパッチを投げていた、 CPython の dict を PyPy の dict と同じような仕組みにするパッチが取り込まれました。 php, Ruby につづいて Python もデフォルトで dict が挿入順を維持するようになりました。

とはいえ、パッチが取り込まれたのが金曜日で、まだどこまでが言語仕様でどこからが CPython の実装詳細なのかは微妙なところです。 今のところの Guido の宣言では、 dict の順序は実装依存で、キーワード引数や名前空間__dict__ は言語仕様として挿入順を維持する (他の Python 3.6 言語実装では dict ではなく OrderedDict などのサブクラスを返すかもしれない) ということになりそうです。

しかし、「そんなことしたら仕様確認せずに動作だけ見て思い込みで実装依存なコードが増えるじゃないか!」「いやいや、 CPython と PyPy がメジャーな実装で、その2つで動けばいいって判断したならそれに頼るのはOKだろ」「micropython はもともと性能(衝突頻度)よりも省メモリ重視でハッシュテーブルをそんなにスカスカにしてないからこの実装に追従してもコンパクトにならないんだけど」などといろんな意見が飛び出していて予断を許しません。

個人的には Guido の方針に賛成です。特に IronPythonJython などの GIL がない実装が Python 3.6 に追いついたときに、マルチスレッドで性能が出やすい実装を選ぶ余地を残しておいて欲しいからです。

ということで、みなさんは「順序を維持してくれたほうが(ログが見やすくなったりとかで)使い勝手いいけど、必須ではない」というところだけ dict の挿入順維持に頼ってください。

なお、 OrderedDict が新しい dict の実装を利用してより省メモリにできるはずですが、 dict の新実装が取り込まれたのが金曜日なので、月曜日に間に合わせるのは妻子持ちには無理だと最初から諦めてましたごめんなさい。

f-string

"{foo} and {bar}".format(foo=foo, bar=bar) を、 f"{foo} and {bar}" と書けるようになりました。 {} の中にはちゃんと式が書けます。やったね! phpRuby に追いついたね!

なお、 f-string 内でのエスケープシーケンスの使い方については制限があります。詳しくは言語仕様 を参照してください。

pathlib が使いやすくなった

pathlib.Path 経由で直接ファイルを読み書きするときは良かったのですが、他のライブラリとかにパスを渡すときは、今までは渡すときに文字列に変換してあげないといけないことが多くてあまり使い勝手が良くありませんでした。

Python 3.6 では os のいろんな関数が pathlib.Path をサポートするようになったので、そのライブラリが引数をそのまま os の関数に渡しているようなときには、文字列への変換が不要になりました。ダックタイピング万歳!

ただし、ライブラリが引数に対して isinstance(file, str) とかして、引数が文字列ならファイルを開いてファイルオブジェクトなら直接使うみたいな分岐をしている場合は、そのライブラリの対応を待たないといけません。

Windows でファイル名やコンソール入出力が utf-8

Pythonは基本的に unicode 推しで、特に Windowsバイト列でファイル名を利用すると Warning を出しつつ、内部では Windows*AAPI を利用していました。

しかし、 Mercurial などのように生まれも育ちも設計思想も unix な、ファイル名をバイト列として扱うツールを Windows で動かすときに、今さらレガシーなエンコーディング使うのは嫌だという話があり、ファイル名とコンソール入出力をバイト列で行ったときは utf-8 <> utf-16 変換を行って W 系 API を呼ぶようになりました。

これで後方互換性に問題がでるアプリのために、環境変数 PYTHONLEGACYWINDOWSSTDIO, PYTHONLEGACYWINDOWSFSENCODING で元の挙動に戻すこともできます。

あと、 Windows 10 Anniversary Update で MAX_PATH の 260 文字制限を超えられるようになりましたが、 Python も問題のある Windows API を使ってなかったので、これに対応するように Manifest が書かれました。 260文字超えたパスを扱えます。

C99 解禁

Python プログラマーには全く関係のない話ですが、 Python を実装するときのC言語の仕様で C99 の幾つかの機能がやーーーっと解禁されました。 cflags にも -std=c99 が追加されます。

例えば、1行コメントが // で書けるようになったとか、 Py_LOCAL_INLINE(void) foo(void)static inline void foo(void) になったりとか、 int16_t が使えるようになったりとか、ローカル変数が関数やブロックの先頭以外の(初期化する位置で)宣言できるようになったりとか、etcetc、新しい Python ハッカーにとっては大幅にリーダビリティが上がったと思います。君も Python ハッカーにならないか?

なお、早速 Python のバグトラッカーに、 CentOS 5 のデフォルトの GCC でビルドが通らない!って報告が来てます。とても Python らしいですね。新しい gcc インストールしてください。というか CentOS 5 とか RHEL 5 とか捨ててください。

高速化

PythonからCの関数を呼び出す時、今まではVMのスタックから引数を取り出してタプルに入れてC関数に渡していました。 それが新しく加わった呼び出し規約では、VMスタックから直接引数を取り出せるようになり、一時オブジェクトとしてのタプルが不要になりました。

この呼び出し規約は人間が直接書くことはあまり意図されていず、 Argument Clinic という、シグネチャを決められたシンタックスで書けば引数解析コードを自動生成してくれるツールを使って対応することになります。 CPython の多くのビルトイン関数はすでにこのツールを使っているので、いろんなCで書かれた関数の呼び出しが速くなったはずです。

また、バイトコードのフォーマットが大幅にかわり、1つの命令が2バイト使うようになりました。(新しいフォーマットを区別するときはワードコードと呼ばれてます。) これにより Python 3.6 でも少し高速化されてるはずですが、本格的に新しいフォーマットの力を引き出すような高速化は 3.7 に持ち越しです。

他にも将来の高速化へ向けた仕込みとして、 dict に(CPython内部からしか使えないプライベートな)バージョン番号が入って名前解決結果をバージョンが変わるまでキャッシュできるようになったり、バイトコードを格納しているコードオブジェクトに co_extra というフィールドが追加されてサードパーティーのプロファイラーやJITエンジンが利用できるようにしたりしてます。

Ruby 3x3 が話題ですが、 Python の高速化からも目が離せません。

Python と Ruby と typing

この話題について補足しておきます。なお、僕はTAPL脱落組なのであまり正確性は期待しないでください。

背景

Ruby Kaigi で Matz が Ruby3 に向けて考え中の静的型について話されたようです。

少し前から、 Python でも Guido が Dropbox での大量のコードベースを改善していくために type hinting がほしいということで PEP 484 を始めました。

ここでは、 Java などが採用している一番よく知られた nominal subtyping, Go などが採用している structural subtyping, PythonRuby の duck typing, そして PEP 484 が採用した Gradual Typing について、違いをかんたんに紹介していきます。

Nominal Subtyping

まずは次のコードを見てください。

interface IFoo {
  void Foo(String s);
}

class Foo implements IFoo {
  public void Foo(String s) {}
}

class Main {
  public static void main(String[] args) {
    IFoo f = new Foo();
    f.Foo("hello");
  }
}

IFoo f = new Foo() では、 IFoo 型の変数 f を宣言し、 Foo 型のインスタンスで初期化しています。 静的に(実行しなくても)変数に型が付いているので、これは静的型付けです。

IFoo 型の変数に、 Foo 型のインスタンスを代入していますが、これは Foo が IFoo のサブタイプだからです。(ちなみにサブタイプの逆はスーパータイプです。)

サブタイプは、スーパータイプが定義しているAPIをすべて提供している必要があります。例えば、スーパータイプが Foo(string) というメソッドを定義しているなら、サブタイプもそのメソッドを提供しなければなりません。 スーパータイプに対して可能な操作がすべてサブタイプに対しても可能なので、スーパータイプの型の変数にサブタイプの値を代入してもスーパータイプと同じように操作できることが保証されます。つまり、 f.Foo("hello")コンパイル時に安全性が検証され、実行時に fFoo(String) というメソッドを持ってないというエラーは起こりません。

さて、ここまでの説明は静的型付けと subtyping についてでした。では nominal subtyping の nominal とは何でしょうか? それは、 Foo implements IFoo のように、サブタイプ関係を宣言によって定義するという意味です。そして、 nominal subtyping と対になるのが structural subtyping です。

Structural Subtyping (構造的部分型)

Nominal Subtyping が implements や extends といった宣言により部分型関係が成り立っていたのに対して、 Structural Subtyping では構造によって部分型関係が成り立ちます。次の例を見てください。

package main

import (
    "fmt"
    "io"
)

type Reader interface {
    Read(buf []byte) (int, error)
}

type StringReader struct {
    s string
}

func NewStringReader(s string) *StringReader {
    return &StringReader{s}
}

func (r *StringReader) Read(buf []byte) (int, error) {
    l := len(r.s)
    if len(buf) < l {
        copy(buf, r.s)
        r.s = r.s[len(buf):]
        return len(buf), nil
    }
    copy(buf[:l], r.s)
    r.s = ""
    return l, io.EOF
}

func ReadPacket(r Reader) ([]byte, error) {
    header := make([]byte, 1)
    if _, err := r.Read(header); err != nil {
        return nil, err
    }
    l := int(header[0])
    data := make([]byte, l)
    n, err := r.Read(data)
    return data[:n], err
}

func main() {
    r := NewStringReader("\x03foo")
    packet, _ := ReadPacket(r)
    fmt.Printf("%s\n", packet)
}

StringReader 型は Reader 型を明示的に継承していませんが、 ReadPacket(r Reader) に *StringReader 型の値を渡せています。

Go のコンパイラは、 *StringReaderReader インターフェースが宣言しているメソッドを全部定義しているので、サブタイプだと判断しています。これが構造による部分型です。

Structural Subtyping の Nominal Subtyping に対するメリットを考えてみましょう。

今回書きたかった関数 ReadPacket(r Reader) が、本当は os.File 型からパケットを読み込むための関数で、 *StringReader は単に ReadPacket をテストするためのモックだったとします。 もし os.File が何もインターフェイスを継承していなかったとしたら、 nominal typing では、 ReadPacket() の引数の型を、 os.File とモックの両方を直接受け取れるように定義することができません。自分で os.File をラップするクラスを作って Reader インターフェイスを継承しないと行けないので面倒です。 また、 os.FileRead(), Write(), Close(), Seek() を定義したインターフェイスを実装していた場合、 ReadPacket()Read() しか使っていないのに、 *StringReader は3つの使ってないメソッドのダミー実装をしないといけませんでした。

構造的部分型は、このように実際に利用しているメソッドだけを定義したスーパータイプを後付けできるのが魅力です。 ライブラリを提供する人も、実装している型が一つだけなのにユーザーがモックを定義しやすくするためにインターフェイスを作らなくてよくなるので、ライブラリのAPIの見通しがよくなるといったメリットもあります。

Duck Typing

duck typing は動的型付け言語による、「とりあえずやってみてエラー無く動いたらオッケー」という型システムです。(というかこれは型システムなのか?)

次のコードを見てください。

def read_packet(s):
    n = s.recv(1)[0]
    return s.recv(n)

このコードは、ソケットのようなオブジェクトを引数に取ることを期待しています。そのオブジェクトは .recv(int) というメソッドを実装していて、バイト列を返すことを期待しています。バイト列に対して [0] するとその整数値を取り、また s.recv(int) を呼び出して、その戻り値のバイト列を返す、という想定です。

このコードは、次のようにして呼び出してもエラー無く実行することができます。

class S:
    def recv(self, x):
        return [x]

print(read_packet(S()))  # => [1]

戻り値の型は bytes を期待していたのに、配列が返ってきましたね。でも、エラーなく実行できたんだから、これでOKというのが duck typing です。 利用者のメンタルモデルとしては、 インターフェイスの定義を省略した structural subtyping (実際にインターフェイスの型の定義を省略できる structural typing の言語があります。後述) のような型がふんわりと頭の中にあると思いますが、実際の duck typing はもっとガバガバです。

この例だけで、一番のデメリットである脆弱性(型が実行時エラーから何も守ってくれない&実行時エラーにすらならず出力が壊れる危険もある)は十分伝わったと思います。

ですが、このデメリットは非実用的というほどは大きくありません。まず、こんなパズル的なコードを書かない限り、ちゃんと実行時エラーで丁寧なエラーメッセージとバックトレースを吐いて止まります。なので、気軽になんども実行できるようなスクリプトであれば、実行時エラーを見ながら修正するサイクルは、ビルドが遅い言語でコンパイルエラーを見ながら修正するサイクルより早いことだってありえます。

一方のメリットは、インターフェイスの定義が要らない分だけコードが簡潔になることです。 Javaインターフェイスを見る時間で、 PythonRuby なら実装を読めてしまうことだってあります。

とはいえ、プログラムが大規模化してくると、リファクタリングツールや静解析ツール等に十分なヒントを与えられないために改修コストがどんどん膨れ上がる危険性があります。 Python が type hinting を導入したのもそれが理由です。 Python 自体が型を使うのではなく(将来そういうモードが追加される可能性はありますが)、 mypy, Jedi, PyCharm などの静解析、コード補完、IDEが型情報を使うので、 typing ではなく type hinting と呼んでいます。

Python の Gradual Typing

Python の type hinting で使われているシステムが PEP 483 で紹介されている Gradual Typing です。 漸進的型付けとは何か も良い紹介記事なので参考にしてください。

ざっくりと言えば、 Gradual Typing とは、 通常の Subtyping に Any という特殊な型を追加したものです。

通常の部分型関係は順序関係になっていて、推移律が成り立ちます。つまり、 B が A のサブクラス、 C が B のサブクラスなら、 C は A のサブクラスでもあります。 (C > B > A) なので、 C 型の値を A 型にアップキャスト(サブタイプからスーパータイプへのキャスト)が可能です。

一方で Any 型は部分型関係の外にいて、すべての型から Any にキャストできますし、 Any からすべての型にキャストできます。ですが部分型関係の外にいるので推移律も当てはまらず、 A が Any にキャストできて、 Any が C にキャストできるからといって、直接 A から C へのキャストが許される訳ではありません。

この Any は type hint がまだついてないとか、任意の型を取りうるという意味になります。例えば JSON の object を dict にするとき、がんばらない型ヒントは Dict[string, Any] になります。(頑張ると Any ではなく Union という「幾つかの型のどれか」型になるのですが、その Union が取り得る型の1つが JSON object を表す dict 型になるので、再帰的な定義をすることになります。)

さて、type hinting をどんどん書いていくと、結局静的型付け言語と同じくらいコードが増えそうな気がします。

しかし実際にはそこまで増えないことが多そうです。というのは、上の例であったような「モックにすげ替えるためだけにインターフェイス型を用意する」必要がないからです。 モックオブジェクトを Any 型にしてしまえば、type hint に具象型を直接書いてる引数にでもモックを突っ込めますし、そもそも mypy などの静解析ツールの対象からテストディレクトリを外してしまうという手だってあります。

Python の type hinting はまだまだ開発途中で実用段階とは言い難いですが、だいたいこんな感じで、動的型の特徴を殺すこと無く静的型のメリットの一つである手厚いツールの支援を取り込む方向で進んでいます。

Ruby Kaigi で語られた構想では、 Python よりももっとプログラマーが書く型情報が減る方向で考えられているようなので、どうなっていくのか期待して見守りたいと思います。

追記: 静的な duck typing

上の例では structural subtyping と duck typing の比較として Go と Python を比べていましたが、実際には structural subtyping は単に部分型に関するルールでしかないので、直接比較できるようなものではありません。

実際に、引数の型をコード上に書かなくても静的な duck typing とよべるような型導出を行ってくれる言語 (OCaml が有名) もあります。つまり、

def read_packet(s):
    n = s.recv(1)[0]
    return s.recv(n)

このコードから、 "s の型は、 .recv(int) というメソッドを持っていて、そのメソッドの戻り地は添字アクセス (x[0]) できるような型で、..." という暗黙の型を実行せずに静解析で導出します。あとはこの関数を呼び出すときの実引数の型が、導出された暗黙の型のサブタイプかどうかを structural subtyping でテストします。

このコードから暗黙の型を導出するステップは静的な duck typing の用にみえますし、動的な duck typing でプログラムしているプログラマーの頭の中にふんわりと存在する型ともかなり一致すると思います。たぶん Matz の soft-typing 構想にも近くて、これが冒頭の Matz の発言につながったのだと思います。

動的な duck typing はもっとガバガバ(引数の型ではなく値によって戻り値の型が変わるなんてこともザラ)なので、この静的な duck typing は動的 duck typing に対する完全な静的型チェックにはならないですが、 Python の Gradual Typing が Any を導入したのと同じようになんらかのクッションを挟むことでうまく適用できる可能性があります。

Python 3.6 に向けて新しい dict 実装の人柱募集

会社のBlogに書いた通り、現在新しい dict の実装を試しています。

また、 shared key を削除して実装を削れた分、別の効率のいい特殊化 dict を実装して、 compact + shared よりも高い効率を狙うこともできます。 今はあるアイデアのPOCを実装中なので、採用されたらまた紹介します。

と書いたのですが、とりあえず Python のテストは完走するようになりました。

github.com

Python-Dev に投稿したメール の通り、 SphinxPython の Doc を make html するのにかかるメモリ使用量は、 Python 3.6a2+ (Python 3.5 と同等の key sharing dict) では 176MB, 上の key sharing を外した新 dict 実装ブランチでは 160MB で、 key sharing を外しても十分に省メモリかできている事が判ります。

Python-Dev では、アプリケーションによってはほとんどの dict がインスタンス__dict__ なんじゃないか、その場合は無視できないリグレッションになるのではないか、という意見がでていて、説得するためにリアルなOOPで書かれたアプリケーションにおけるメモリ使用量のデータをもっと集めたいところです。

そこで、興味があって計測に適したアプリケーションを用意できる人は interned-dict ブランチ を試してみていただけないでしょうか?

計測に適したアプリケーションは次のとおりです。

  • OOPで書かれていて、かなりの数のインスタンスを作る
  • Python 3.6a2 と上記のブランチの2つのプロセスで同じ状況を再現でき、その時のメモリ使用量を計測できる
  • 繰り返して安定した結果を得られる

また、 Sphinx のビルドのように CPU バウンドで数分で実行できて実行時間が安定しているアプリケーションであれば、 /usr/bin/time を使って maxrss と同時に実行時間も計測していただけると助かります。 (その際、比較対照の 3.6a2 は同じ configure オプション、同じ環境でビルドしたものでおねがいします。)

計測結果は、

  • どんな種類のアプリケーションか (Webアプリ、GUIアプリ等)
  • どんなライブラリ・フレームワークを使っているか
  • どうやって計測したか
  • (アプリケーションを公開可能な場合)完全な再現手順

をつけて、

[Python-Dev] Idea: more compact, interned string key only dict for namespace.

に返信するか、上記のプルリクエストにコメントしてもらえれば私が転載します。

また、万が一クラッシュしてしまった場合も、上のプルリクエストに (再現可能なら faulthandler を有効にした状態で得たスタックトレースをつけて) コメントしてください。

manylinux1 wheel を作ってみる

先日の記事 で紹介した、 manylinux1 wheel を作ってみます。

manylinux1 docker image

ビルド環境を Docker image として公開してくれています。

docker pull quay.io/pypa/manylinux1_x86_64; docker pull quay.io/pypa/manylinux1_i686 しておきましょう。

なお、このイメージは CentOS 5 をベースにビルドスクリプトを実行しているだけなので、 vagrant 等で同じ環境を作るのは簡単そうです。

作業ディレクトリを作って docker 内で bash を起動します。

$ mkdir docker
$ docker run --rm -ti -v `pwd`/docker:/docker  -w /docker quay.io/pypa/manylinux1_x86_64 bash

このように実行すると、bash を抜けるときにイメージが消えますが、ボリュームに指定した docker ディレクトリの内容は残るので便利です。

wheel をビルドしてみる

/opt/python 配下に各種 Python が用意されています。 Python 2.6 と 2.7 は、 utf-16 の narrow build (cp26m, cp27m) と utf-32 の wide build (cp26mu, cp27mu) があります。

[root@c3f621332eef docker]# ls /opt/python/
cp26-cp26m  cp26-cp26mu  cp27-cp27m  cp27-cp27mu  cp33-cp33m  cp34-cp34m  cp35-cp35m

試しに msgpack の最新リリース版をビルドしてみます。

[root@89ab8fbbfbc0 docker]# curl 'https://pypi.python.org/packages/a3/fb/bcf568236ade99903ef3e3e186e2d9252adbf000b378de596058fb9df847/msgpack-python-0.4.7.tar.gz' -o msgpack-python-0.4.7.tar.gz
[root@89ab8fbbfbc0 docker]# tar xf msgpack-python-0.4.7.tar.gz
[root@89ab8fbbfbc0 docker]# cd msgpack-python-0.4.7
[root@89ab8fbbfbc0 msgpack-python-0.4.7]# /opt/python/cp35-cp35m/bin/python setup.py bdist_wheel
...
[root@89ab8fbbfbc0 msgpack-python-0.4.7]# /opt/python/cp34-cp34m/bin/python setup.py bdist_wheel
...
[root@89ab8fbbfbc0 msgpack-python-0.4.7]# /opt/python/cp27-cp27m/bin/python setup.py bdist_wheel
...

[root@89ab8fbbfbc0 msgpack-python-0.4.7]# rm -rf build
[root@89ab8fbbfbc0 msgpack-python-0.4.7]# /opt/python/cp27-cp27mu/bin/python setup.py bdist_wheel

Python 2.7 や 2.6 のビルド時は、 rm -rf build をしておかないと、 narrow / wide ビルドが違っても build/ ディレクトリ配下にできるファイルにはタグがなくて再利用されてしまうので注意が必要です。

これで dist ディレクトリに、通常の linux 用 wheel ができます。しかしこのままでは PyPI にはアップロードできません。通常の linux wheel は、ローカルや同じ環境のマシンで使い回すためにしか使えません。

manylinux1 wheel に変換する

Docker イメージには auditwheel コマンドが用意されています。このコマンドで、 wheel 内のバイナリが、決められた以外の外部ライブラリに依存してないかなどのチェックが行われます。 (auditwheel も manylinux1 もまだ新しいツールなので、チェックが通ったから安全とは限りません。たとえば上で説明した narrow / wide ビルドミスは現状ではチェックが漏れています。)

auditwheel show コマンドでチェック結果が表示され、 auditwheel repair コマンドは依存してる外部ライブラリをバンドルして RPATH を設定するなどの黒魔術を施して linux wheel を manylinux1 wheel にリネームしてくれます。 repair コマンドは wheelhouse ディレクトリに wheel を生成します。

(repair コマンドの黒魔術が必要ない場合は、 上の手順で bdist_wheel するときに -p manylinux1_x86_64 オプションを使えば良さそうです。)

[root@0018d7cfd7df dist]# auditwheel show msgpack_python-0.4.7-cp27-cp27m-linux_x86_64.whl

msgpack_python-0.4.7-cp27-cp27m-linux_x86_64.whl is consistent with
the following platform tag: "manylinux1_x86_64".

The wheel references the following external versioned symbols in
system-provided shared libraries: GLIBC_2.2.5.

The following external shared libraries are required by the wheel:
{
    "libc.so.6": "/lib64/libc-2.5.so",
    "libgcc_s.so.1": "/lib64/libgcc_s-4.1.2-20080825.so.1",
    "libm.so.6": "/lib64/libm-2.5.so",
    "libpthread.so.0": "/lib64/libpthread-2.5.so",
    "libstdc++.so.6": "/usr/lib64/libstdc++.so.6.0.8"
}


[root@c89bce940544 dist]# for i in *.whl; do auditwheel repair $i; done
Repairing msgpack_python-0.4.7-cp27-cp27m-linux_x86_64.whl
Previous filename tags: linux_x86_64
New filename tags: manylinux1_x86_64
Previous WHEEL info tags: cp27-cp27m-linux_x86_64
New WHEEL info tags: cp27-cp27m-manylinux1_x86_64

Fixed-up wheel written to /docker/msgpack-python-0.4.7/dist/wheelhouse/msgpack_python-0.4.7-cp27-cp27m-manylinux1_x86_64.whl
Repairing msgpack_python-0.4.7-cp27-cp27mu-linux_x86_64.whl
Previous filename tags: linux_x86_64
New filename tags: manylinux1_x86_64
Previous WHEEL info tags: cp27-cp27mu-linux_x86_64
New WHEEL info tags: cp27-cp27mu-manylinux1_x86_64

Fixed-up wheel written to /docker/msgpack-python-0.4.7/dist/wheelhouse/msgpack_python-0.4.7-cp27-cp27mu-manylinux1_x86_64.whl
Repairing msgpack_python-0.4.7-cp34-cp34m-linux_x86_64.whl
Previous filename tags: linux_x86_64
New filename tags: manylinux1_x86_64
Previous WHEEL info tags: cp34-cp34m-linux_x86_64
New WHEEL info tags: cp34-cp34m-manylinux1_x86_64

Fixed-up wheel written to /docker/msgpack-python-0.4.7/dist/wheelhouse/msgpack_python-0.4.7-cp34-cp34m-manylinux1_x86_64.whl
Repairing msgpack_python-0.4.7-cp35-cp35m-linux_x86_64.whl
Previous filename tags: linux_x86_64
New filename tags: manylinux1_x86_64
Previous WHEEL info tags: cp35-cp35m-linux_x86_64
New WHEEL info tags: cp35-cp35m-manylinux1_x86_64

Fixed-up wheel written to /docker/msgpack-python-0.4.7/dist/wheelhouse/msgpack_python-0.4.7-cp35-cp35m-manylinux1_x86_64.whl

PyPI にアップロード

docker で実行していた bash を抜けると、 docker イメージは消えますが、ボリュームの中に wheel が残っています。これを twine を使ってアップロードします。

$ pip install twine
$ cd docker/msgpack-python-0.4.7/dist/wheelhouse/
$ ls
msgpack_python-0.4.7-cp27-cp27m-manylinux1_x86_64.whl  msgpack_python-0.4.7-cp34-cp34m-manylinux1_x86_64.whl
msgpack_python-0.4.7-cp27-cp27mu-manylinux1_x86_64.whl msgpack_python-0.4.7-cp35-cp35m-manylinux1_x86_64.whl
$ twine upload *.whl
Uploading distributions to https://pypi.python.org/pypi
Uploading msgpack_python-0.4.7-cp27-cp27m-manylinux1_x86_64.whl
Uploading msgpack_python-0.4.7-cp27-cp27mu-manylinux1_x86_64.whl
Uploading msgpack_python-0.4.7-cp34-cp34m-manylinux1_x86_64.whl
Uploading msgpack_python-0.4.7-cp35-cp35m-manylinux1_x86_64.whl

あとは同じ手順を i686 の方の docker でもすれば x86 対応も簡単そうです。

終わりに

msgpack は libstdc++ に依存してしまっていますが、これは PEP 513 で定義されている Core shared library に含まれているので、多分問題ないと思います。

一方 mysqlclient-python の場合、 libmysqlclient 経由で ssl などに依存していて、これは core shared library に含まれていないので、 static link や bundle が必要になります。特に ssl のようにセキュリティアップデートが頻繁にあるライブラリの場合は wheel 提供者が責任をもって追随アップデートする必要がありそうで面倒です。

Core shared library に含まれていないライブラリを利用している場合は、 PEP 513 を良く読んで、ちゃんと理解できないのであれば、 manylinux1 wheel の提供は危険です。 wheel builders という ML ができたので、そこで相談してみるのが良いと思います。

Wheel-builders Info Page

追記: sdist から wheel を作るより簡単な手順

上の手順では sdist の tar.gz を展開してから setup.py を実行していましたが、 pip wheel コマンドを使うとこの流れを自動化できます。 作業ディレクトリも毎回作り直されるので、 build ディレクトリを消し忘れて narrow / wide 互換性問題を起こすことも無いはずです。

bash-3.2# for i in cp35-cp35m cp34-cp34m cp27-cp27m cp27-cp27mu; do /opt/python/$i/bin/pip wheel --build-option='-pmanylinux1_i686' msgpack-python-0.4.7.tar.gz ; done;

/opt/_internal/cpython-3.5.1/lib/python3.5/site-packages/pip/commands/wheel.py:126: UserWarning: Disabling all use of wheels due to the use of --build-options / --global-options / --install-options.
  cmdoptions.check_install_build_global(options)
Processing ./msgpack-python-0.4.7.tar.gz
Building wheels for collected packages: msgpack-python
  Running setup.py bdist_wheel for msgpack-python ... done
  Stored in directory: /docker
Successfully built msgpack-python
...
...
Successfully built msgpack-python
bash-3.2# ls -la
total 1108
drwxr-xr-x  1 1000 ftp     238 Apr 27 08:52 .
drwxr-xr-x 39 root root   4096 Apr 27 08:46 ..
-rw-r--r--  1 1000 ftp  241090 Apr 27 08:51 msgpack_python-0.4.7-cp27-cp27m-manylinux1_i686.whl
-rw-r--r--  1 1000 ftp  241106 Apr 27 08:52 msgpack_python-0.4.7-cp27-cp27mu-manylinux1_i686.whl
-rw-r--r--  1 1000 ftp  261276 Apr 27 08:51 msgpack_python-0.4.7-cp34-cp34m-manylinux1_i686.whl
-rw-r--r--  1 1000 ftp  256676 Apr 27 08:51 msgpack_python-0.4.7-cp35-cp35m-manylinux1_i686.whl
-rw-r--r--  1 1000 ftp  126251 Apr 27 06:52 msgpack-python-0.4.7.tar.gz

ここまで行けば、 docker で bash を実行する代わりに、 wheel をビルドするスクリプトを直接実行できそうですね。

文字列の先頭への追加に対する最適化を期待していいか?

paulownia.hatenablog.com

文字列型が immutable な言語では、文字列にループで文字を追加していくようなコードは、 Java でいえば StringBuilder などの方法を使えというのが昔から言われていた。しかし、 Java でも Python でも、文字列の右側に追加していくコードは最適化されるようになっており、現代では処理系の最適化を前提に出来るなら、読みやすくなる方が良いという風に常識が変わってきていると思う。

しかし、今回クソコード呼ばわりした left-pad のループは、文字列の左側への追加だ。静的型を元にコンパイル時に最適化出来る言語ならともかく、実行時に振る舞いを見ながら最適化するような言語で、処理系が文字列の先頭への追加を最適化してくれることを前提にして良いものだろうか?少なくとも Python はそこまではやってくれない。node.jsではどうだろう?

追記:記事を公開した時に書いていたコードにバグがあり、結論が180度変わりました!

'use strict';

function leftpad1(str, len, ch) {
  //str = String(str);

  var i = -1;

  if (!ch && ch !== 0) ch = ' ';

  len = len - str.length;

  while (++i < len) {
    str = ch + str;
  }

  return str;
}

function leftpad2(str, len, ch) {
  str = String(str);
  if (!ch && ch !== 0) ch = ' ';

  len -= str.length;
  if (len < 1) {
    return str;
  }
  var pad = "";
  for (var i=0; i<len; i++) {
    pad += ch;
  }
  return pad + str
}

function leftpad_repeat(str, len, ch) {
  str = String(str);
  ch = String(ch);
  if (!ch && ch !== 0) ch = ' ';

  len -= str.length;
  if (len < 1) {
    return str;
  }
  return ch.repeat(len) + str;
}

const count = 1000000;

for (var len=4; len<=128; len*=2) {
  console.log("pad length", len);

  console.log("leftpad1", leftpad1('aaaa', len, 0));
  console.log("leftpad2", leftpad2('aaaa', len, 0));
  console.log("repeat  ", leftpad_repeat('aaaa', len, 0));

  console.time('leftpad1');
  for (let i = 0; i < count; i++) {
    leftpad1('aaaa', len, 0);
  }
  console.timeEnd('leftpad1');

  console.time('leftpad2');
  for (let i = 0; i < count; i++) {
    leftpad2('aaaa', len, 0);
  }
  console.timeEnd('leftpad2');

  console.time('leftpad_repeat');
  for (let i = 0; i < count; i++) {
    leftpad_repeat('aaaa', len, 0);
  }
  console.timeEnd('leftpad_repeat');
}

結果 [ms]

padding len 4 8 16 32 64 128
left-pad 21 118 265 491 959 1755
左追加除去版 19 81 273 483 875 1623
repeat 38 98 145 171 197 215

結論: やはり左追加を最適化してくれることを前提にするのは間違っている。

結論: 元の実装でも十分速い。JS処理系の文字列処理最適化は相当賢い。クソコードとか言ってごめんなさい。

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