Homebrew の Python で何が変わって何がもとに戻ったのか

rcmdnk.com

大分混乱した状態になってしまったので、今年何が変わってきたのか、今回の変更でどこまでもどったのかを整理しておきます。

1/19

python という formula が python コマンドをインストールしなくなりました。 python コマンドを起動すると、通常は /usr/bin/python が起動するようになりました。

1.5.0 — Homebrew

3/2

python という formula が Python 3 になり、 Python 2.7 は python@2 になりました。

python formula (Python 3) が python コマンドをインストールするようになったので、 python コマンドを起動すると通常は Python 3 が起動するようになりました。これが npm の gyp とか色んな所をぶっ壊す変更になっていました。

一方 python@2 formula は keg-only になっていたので、デフォルトではコマンドがインストールされず、必要に応じて brew link --force python@2 などする必要がありました。

コマンド名以外の変更として、多くの formula から depends_on "python" が消されました。今までは依存関係で python (Python 2) がインストールされることが多かったのが、システムの Python を使うようになります。

しかし、 vim など一部の formula では depends_on "python" が残っています。これらは Python 3 に依存するようになりました。

前回の記事

3/10

python@2 が keg-only でなくなりました。 python formula は python3 コマンドだけを提供し、 python@2 formula が pythonpython2 コマンドを提供するようになりました。 python コマンドが /usr/bin/python でなく Homebrew の Python 2 を起動するということで、この点については 1/19 以前の状態にまで戻りました。

1/19 以前の状態と現在の状態を比べると、次のようになります。

  • Python 2 の formula 名が python から python@2 になり、 Python 3 の formula 名が python3 から python になった。
  • 多くのパッケージから depends_on "python" が消えた。依存で Python がインストールされることが減り、代わりに macOSの /usr/bin/python が使われるようになった。 brew install python@2 をすることで macOS ではなく Homebrew の Python 2を使うことも可能で、そうすれば今まで通りの動作になる。
  • vim, macvim など幾つかのパッケージは、 depends_on "python" のまま、 Python 2 依存から Python 3 依存に切り替わった。オプションで Python 2 を使うようにビルドすることもできるが、 bottle が提供されるのはデフォルトの Python 3 依存版。

最終的に一番妥当な形に落ち着いたと思います。

これから 2020 年に向けて、 Python 3 をサポートしているソフトウェアには depends_on "python" を追加して、 Python 2 ではなく Python 3 上で動くようにしていくと良いと思います。

3月1日、Homebrew のデフォルトの Python が Python 3 になります。

以前からアナウンスされていた 通り、 3/1 (日本時間では 3/2 になるかも)にデフォルトの PythonPython 3 に切り替わる予定です。

現在そのプルリクエストがレビュー中です。

github.com

具体的には、今まで "python" という formula は Python2.7 でしたが Python 3.6 になります。 Python 2 をインストールするには brew install python2 もしくは brew install python@2 とします。(これまで使えていた python3 という Formula は python への alias になります。)

何らかの理由で意図的に Python 2 を選択しているユーザー以外は、 brew install python で(推奨される)Python 3をインストールできるようになるので、これは大きな前進です。

また、 Python 2 か 3 のどちらかに依存するパッケージが、今までデフォルトで Python 2 に依存していたのが、 Python 3 に依存するようになります。例えば vim をインストールする時、今までは brew install vim では Python 2 が有効になっていて、 brew install vim --with-python3 とすると Python 3 を有効にできるものの bottle (ビルド済みのバイナリパッケージ)が利用できずソースからビルドになってしまいました。それがこの変更でデフォルトで Python 3 が有効になります。

これで多くの vim, macvim ユーザーが +python から +python3 な vim に移行するので、vim プラグイン作者はより容易に Python 2 を切れるようになります。個人的に一番お世話になっているので例として vim を挙げていますが、多くのアプリで同じことがいえます。

Zenfone 4 を買ったけどスクフェスがダメだった

今のメイン端末は Zenfone 3。 会社の補助制度が使えたのと、最近アズールレーンなるゲームをやっててローディングや一覧画面のスクロールの重さに耐えかねて。

ZF3 の Snapdragon 625 はなかなかいいバランスのSoCではあるのだけれど、シングルスレッド性能がよろしくない。それがローディングの遅さにつながってるんだろうと踏んだ。ZF4 の Snapdragon 660 は、Snapdragon 625 に比べてシングルスレッド性能が倍になっているので期待。

まだ購入して数日でメイン端末を移行してないけど、感想。

  • 5.2" -> 5.5" に大きくなったので、大きさというか迫力がすごい。メインを切り替えたらなれるんだろうけど。付属のTPUケースをつけたら更にすごい。
  • 背面フラットは魅力に思ってたんだけど、今はケースつけたままにしてるので特に意味はない。
  • 画面分割で割合を変更するのに表示サイズを小にする必要があるのを今更知った。文字小さくなるので通常サイズでOKにしてほしい。
  • 指紋センサーが認識してくれない率がZF3から改善してない。ZF3では指紋登録画面でセンサーを触るとどの指紋に判断されてるかわかったので反応しない指紋を消して登録し直しできたんだけど、ZF4はホームボタン兼用になってるためにそれをやるとホーム画面に戻ってしまう。
  • アズールレーンはローディング倍速とは言わないものの快適になった気がする。あと今までフレームレートが低かった画面で多分60fpsのヌルヌルになってる事が結構あって、30fpsで良いからバッテリー消費抑えてくれと思ったり。
  • メモリが3GB→6GBに倍増してて、メールとか読んだあとでもゲームが落ちてない事が多いのも快適。
  • スクフェスのライブがダメ。ZF3ではエキスパート曲ほとんどフルコンしてるんだけど、ZF4じゃ全然できない。ノーツの速度が変動する。Nexus 5Xでも大丈夫だったはずなので、ASUSがサウンド周りでなんか余計なことをしたとしか思えない。

スクフェスの件が致命的でメイン端末の切り替えをどうするか悩み中。Oreoへのアップデートが上半期中に予定されているはずなので、それで直ると良いんだけど。

他のスクフェスプレーヤーたちが同じ罠にはまらないように記事に書いておく。


追記。ZF3にも4にもアップデートが来た。 ZF3 が Oreo になって劣化して、 ZF4 は変速がなくなって、同等になった気がする。 タイミング調整は両方共 -10 にしている。

Python の正規表現で IGNORECASE するときは気をつけよう

Python 3 で文字列が Unicode になりました。というだけで感のいい人は分かるかもしれません。

はい、大文字小文字の判断も ASCII じゃなくて Unicode になります。

In [6]: re.match("[a-z]", 'ı', re.I)
Out[6]: <_sre.SRE_Match object; span=(0, 1), match='ı'>

この文字は LATIN SMALL LETTER DOTLESS I だそうです。

予想外のものにマッチするのは単純にバグになりやすいのもそうですが、この [...] にマッチする部分を作るのも非効率的になります。Python の標準ライブラリの正規表現は最終段階以外が全部 pure Python で書かれているので、正規表現コンパイルが遅く非効率になります。

なお Python の標準ライブラリの正規表現は文字列だけでなくバイト列にも使えて、その場合はこういった罠はありません。

Go が for ループをやめるために足りないもの

ジェネリクスの話題になると常に出てくるのが、 for ループの代わりに関数型スタイルで書きたいという要望です。 for ループで書くのは、可読性が悪く、筋力がいるとまで言う人もいます。

しかし、ジェネリクスが追加されても、このスタイルのプログラミングは実用的にはなりません。ジェネリクス以外にも足りない部分がたくさんあるのです。

例えば、次のようなコードを考えてみましょう。

type PointLog struct {
    ID     int64
    UserID int64
    Point  int32
}

// 今の書き方
func UserTotalScore(log []PointLog, userID int64) int64 {
    var t int64 = 0
    for _, p := range log {
        if p.UserID == userID {
            t += int64(p.Point)
        }
    }
    return t
}

ジェネリクスが入ったとして、 Filter, Map, Reduce で実装してみましょう。構文は仮とします。 なお、コンパイルが通らないコードなので、間違いがあっても気にしないでください。だいたいこういうコードになるという雰囲気だけ見てください。

func[T] Filter(f func(T) bool, xs []T) []T {...}
func[T,U] Map(f func(T) U, xs []T) []U {...}
func[T] Reduce(f func(T,T) T, start T, xs []T) T {...}

// 関数型スタイルの書き方?
func UserTotalScoreFP(log []PointLog, userID int64) int64 {
    return Reduce(func(x, y int64) int64 { return x + y }, 0,
        Map(func(p PointLog) int64 { return int64(p.Point) },
        Filter(func(p PointLog) bool { return p.UserID == userID }, log)))
}

可読性があがり、筋力がいらなくなり…ませんね。短い関数を省略形で書く、ラムダ式と呼ばれたりする記法が足りません。また、ラムダ式の引数や戻り値の型を書かなくて良いようにするためには、型推論の機能も全然足りません。

なお、ここまででも、「実用」するならコンパイラの最適化機能を向上しないといけないかも知れません。 ループで書いたときに比べて、大量の関数呼び出しと、それに伴う一時変数が増えます。上の例では簡単のために引数をスライスで受け渡ししていますが、こういう一時スライスはループで書くには明らかに冗長で、現在の典型的なGoのコードには出現しないものです。そのため、今のGoの最適化機能ではこの一時スライスを除去できません。

スライスの代わりに、値を逐次的に取り出せる一時オブジェクトを使う方式も考えられますが、それも今のGoの最適化機能では for ループと同じ効率になるとは限りません。

さて、ジェネリクスと最適化機能に加えて、ラムダ式とそのための型推論をGoに追加したとしましょう。ループで書いたときと比べてみてください。

// 関数型スタイルの書き方
func UserTotalScoreFP(log []PointLog, userID int64) int64 {。
    return Reduce((x,y) -> x+y, 0,
        Map((p) -> int64(p.Point),
        Filter((p) -> p.UserID == userID, log)))
}

大分スッキリしてきましたね。でも、処理の流れが右から左(サンプルコードでは改行してるので下から上)になっているので、これを左から右(上から下)に変えるJavaのStream APIのようなものを用意したほうがより多くの人にとって可読性が高くなるかも知れません。ついでに Stream API から mapToInt と IntStream も借りて、Reduceの代わりにSumを使いましょう。

// Stream APIもどきを使った書き方
func UserTotalScoreStream(log []PointLog, userID int64) int64 {
    return Stream(log).
        Filter((p) -> p.UserID == userID).
        MapToInt64((p) -> int64(p.Point)).
        Sum()
}

さて、元のコードをもう一度書いておきます。どれくらい可読性が悪く、筋力が必要だったでしょうか?

// 今の書き方
func UserTotalScore(log []PointLog, userID int64) int64 {
    var t int64 = 0
    for _, p := range log {
        if p.UserID == userID {
            t += int64(p.Point)
        }
    }
    return t
}

脱線しますが、個人的にはPythonHaskellの内包表記が、可読性が高く必要な筋力も少ないと思います。

user_totalscore = sum(x.point for x in log if x.userID == userID)

Goに移植するとしたら、上のジェネレータ内包表記(やHaskellの遅延リストの内包表記)は難しいので、リスト内包を追加するのが良さそうです。Pythonの構文を借りるなら、

func SumInt32(xs []int32) int64 {...}  // 戻り値が int64 なのがポイント

t := SumInt32([]int32{x.Point for _, x := range log if x.UserID == userID})

この SumInt32 が不格好に見えるかも知れませんが、それを Sum にするのにはジェネリクスではなくオーバーロードでも可能です。 例えば上の例では引数が int32 の配列ですが、オーバーフローを考えると合計は int64 にしたいかもしれません。こういった引数と戻り値の型が非対称な同名の関数を作りたいならオーバーロードのほうがシンプルです。

オーバーロードの proposal も出ていますが、さて、ジェネリクスオーバーロード、両方追加するべきでしょうか?僕にはわかりません。脱線はここまでにします。


さて、まとめます。関数型スタイルの書き方を現実的にするには、Goを次のように強化する必要がありました。

これらの機能を全部入れようとすると、コンパイラとリンカを複雑にし、コンパイル時間とバイナリサイズを大きくする危険性があります。

なお、私は言語設計者ではないので、まだ足りない大きな部分があるかも知れません。気づいた方は教えてください。


教えてもらったもの

今の panic() はあまり推奨されない機能だし、中断機構を実装するにもなにかしら言語に影響が出そうですね。

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