(PEP 584) dict + dict 演算子追加について

注意:この記事は議論中の機能について紹介し自分の考えを述べるものです。 Python 3.8 で追加されるらしいよ!みたいな拡散はしないでください。

Python-ideas で dict + dict が提案され、PEPになった。

www.python.org

d3 = d1 + d2 の動作は、 d3 = {**d1, **d2}d3 = d1.copy(); d3.update(d2) と同じになる。

もともとの提案では + と | の両方が考慮されていたのだけれども、 sequence + sequence は交換法則を満たさず、 set | set は交換法則を満たすので、交換法則を満たさない dict + dict は | より + のほうが良いだろうという Guido の一言が PEP 化の直前にありこの方向になった。他には | が + よりも初心者に優しくない記号だからなんて理由もあった。ただし、Guidoはその後やっぱり | も良いかもしれないと発言しているので今後は分からない。

追加のモチベーションについて

  • seq + seq や set | set があるのに dict を結合する演算子がないのは不公平。
  • d3 = {**d1, **d2} はわかりにくい。
  • d3 = d1.copy(); d3.update(d2) は複数の statement に分かれるのが嫌。 expression で書きたい。

僕の考え

演算子を abuse するにしても、 seq + seq や set | set はそれぞれ abuse するのに妥当な演算子を選んできた。 seq + seq は、数学未満の算数で考えたとき、 concatenation は「足し算」が現れる場面の1つであろう。また seq + seq + seq と seq * 3 が同じになる。 set | set はもちろん∪の代わりに、ビットごとのOR演算子 | を採用したものだ。集合の和とビットごとの論理和の振る舞いには強い関連がある。

一方で、 dict.update() は seq + seq や set | set に比べて + や | 演算子との関連度が低い。(この関連度の低さをML上でうまく英語で説明できなくてつらい。) dict.update() は dict の key に注目したときに set | set と同じふるまいをする分、 seq + seq よりも set | set に近いふるまいだ。

言語設計は判断の積み重ねで、その中で生まれてきた一貫性は、たとえ意図せず生まれた偶然の産物であったとしても、その言語のユーザーに指針を与え、学習速度を助ける。 |が交換律を満たしていたというのも、振る舞いから適当な演算子を選んでいたというのも、最初から絶対的な指針があった上でこうなっているわけでは無いものの重要な一貫性だ。さてどっちの一貫性を壊す?

もしここで大きな決断をするのであれば、 + を「コンテナ型の連結演算子(連結時の振る舞いはコンテナ依存)」として一般化してしまうのが良いと思う。その場合は set + set も set | set のエイリアスとして追加する。

いくつかの演算子オーバーロードがある最近人気の高い言語を調査してみた結果、「コンテナ型の連結」に共通の演算子を割り当てている言語があった。 Kotlin は (| に対する演算子オーバーロードがないという事情もあるが) + を利用していて、 Scala は ++ を利用している。

破壊的変更メソッドと演算子について

この提案に賛同する人のモチベーションの一つが、 Python の update() メソッドが self を返さず、 func(d1.update(d2))(d1.copy().update(d2))のようなことができないという物がある。

この背景には、実行効率や安全さを1つの複雑な書ける事よりも優先するという設計上の決定がある。そのために Python は、半ば意図的に、いくつかの場面で簡単な処理でも1つずつ文にしないといけないという面倒さを押し付けている。

僕は、言語を使うときの何かの面倒さは、なにかのメリットを得るための引き換えだと、わりとすんなり許容するほうだ。(Goのループとか) だから Python の dict.update() も、この利用頻度なら別に文を書かないといけなくても大した問題じゃないと思っている。

一方でそういうのがとても嫌な人がいることも理解している。しかし、演算子というのはメソッドよりも言語設計上重要な要素のハズで、メソッドの安全性のための設計による面倒さを回避するために演算子を abuse しようというのは、どうも気持ち悪く感じる。d3 = d1.updated(d2) とかのメソッドを足すだけで我慢してほしい。

MySQL Connector/C の代わりに MariaDB Connector/C を使う

mysqlclientWindows 版バイナリ wheel を作るために、以前は MySQL Connector/C を使っていたのですが、しばらく問題があって利用できませんでした。

  • static link library が提供されない
  • ビルドの仕方がよくわからん。ドキュメントもない。
  • TLS と sha256_password / caching_sha2_password のために OpenSSL がいる

ちなみに、 MySQL Connector/PythonWindows 版は site-packages 直下 (つまりグローバル) に OpenSSL の dll を含めて必要なものを全部ぶちまけるようになっています。ロックだ。それと同じロックな手段をとると当然衝突します。

そこで MariaDB Connector/C の動向をウォッチしていたのですが、最近色々な条件が揃ったので、 mysqlclient の Windows 版は MariaDB Connector/C を使うようにしました。

  • static link をサポートしている
  • ビルドの仕方が(一応)ドキュメントされてる
  • TLS も sha256_password や caching_sha2_password も OpenSSL ではなく Windows API を利用しているので OpenSSL の同梱が不要。

MariaDB Connector/C は Windows 版バイナリを配布しているのですが、 caching_sha2_password などのプラグインが DLL の状態で同梱されているので、完全 static link したい場合にはちょっとあいません。

wiki に完全 static なライブラリのビルド方法を残しておいたので、必要な方は参考にしてください。

go-sql-driver/mysql の QueryContext でコンテキストをキャンセルしたら race が起こる

タイトルの通り、 QueryContext の第一引数に渡した Context を、 Rows.Close() を呼び出す前にキャンセルすると、 race が起こる可能性があります。

修正する pull request を作成したのですが、メンテナが作ったプルリクエストは他のメンテナのレビューなしにマージしてはいけないというルールがあるのでまだマージしていません。なので、 QueryContext を利用するときは注意してください。

github.com

問題になるコード

Rows.Scan() の引数の型が *sql.RawBytes (sql.RawBytes の定義は type []byte RawBytes) だった場合、受け取った []byte の中身が他の goroutine から書き換えられることがあります。

rows, err := db.QueryContext(ctx, "select ...", ...)
if err != nil {
    // ...
}
defer rows.Close()

for rows.Next() {
    var sql.RawBytes
    err := rows.Scan(&buf)
    // どこかのタイミングで ctx がキャンセルされると、
    fmt.Println(buf) // buf の中身が他の goroutine から書き換えられる
}

背景

database/sql の設計は、 driver の API は1つの接続だけを扱う(コネクションプール部分は全て database/sql が解決する)、そしてその1つの接続に対して並行で driver の API が呼ばれることはない、ようになっています。

また、 rows.Scan()sql.RawBytes を出力する場合、その参照先の部分は次の rows.Next()rows.Close() 以降はドライバが再利用できるようになっています。これは受信バッファの中身を直接スライスで返すことで余分なアロケートとコピーを減らすためです。

コンテキスト対応が入るまではこれでうまくいっていました。しかし、 database/sql がコンテキストに対応する時に、コンテキストのキャンセルを別 goroutine で監視して、アプリケーションからの rows.Close()rows.Scan() を待たずにドライバー側の rows.Close() を呼び出すようにしてしまったのです。

Go 1.10 の changelog

結果として次のような状況になりました。

  • rows.Scan() が返した sql.RawBytes (type []byte RawByte)は受信バッファの中を直接参照しています。アプリケーションは次の rows.Next()rows.Close() を呼び出すまではその中身を合法的に読むことができます。
  • ドライバは rows.Close()rows.Next() が呼ばれた場合、前回の Scan() で返した []byte の中身を破壊していいので、送受信バッファとして再利用します。
  • database/sql はコンテキストがキャンセルされると別 goroutine からドライバ側の rows.Close() を呼び出します。

正直、なんてことしてくれたんだ!って感じです。アプリケーションが rows.Next() を呼ぶまで待ってくれたらいいのに。なんでわざわざ別 goroutine で。。。

たぶん一刻も早くキャンセルしたかったんでしょうが、MySQLでは百害あって一理ありません。 MySQL でクエリのキャンセルは難しいのでまだ実装してないし、たとえ実装していたとしても、 rows.Next() が一回でも呼ばれているということはクエリの実行はすでに終わっています。 rows.Next() を待たずにキャンセルするメリットが現在も将来も一切ありません。

2019-04-03 追記: sql.RawBytes を使わなくても race が起こる

ユーザーコードに直接受信バッファの中身を参照する []byte が渡されるのは sql.RawBytes を利用したときだけです。

しかし、 database/sqlsql.Rows.Scan() は、 driver.Rows.Next() が返した値と Scan の引数の間の変換処理をしています (convertrows)。この変換処理は driver.Rows.Next() が返した受信バッファ内を直接参照する []byte から string を作ったり、新たな []byte を作ってそこにコピーしたりしています。

タイミング悪くこの変換処理中にコンテキストがキャンセルされた (厳密には database/sql 内の監視 goroutine がコンテキストのキャンセルを検出して driver.Rows.Close() を呼び出した) 場合は、ユーザーコードで sql.RawBytes を使っていなくても race が発生します。

Sony の SBH90C レビュー

https://www.amazon.co.jp/dp/B07CZQMYKK/ref=as_li_ss_tl?&hvadid=274798673996&hvpos=1o3&hvnetw=g&hvrand=13734372561970267267&hvpone=&hvptwo=&hvqmt=&hvdev=c&hvdvcmdl=&hvlocint=&hvlocphy=1028853&hvtargid=pla-465703230864&th=1&psc=1&linkCode=ll1&tag=py0d-22&linkId=9446964464023e4d4ba0cec75ac27735&language=ja_JP

それまでは Anker の Zolo Liberty を使っていたけれども、遅延が激しくてアニメ鑑賞でも気になっていたので買い替えた。 Zolo Liberty は codec が AAC なうえに、完全ワイヤレスだから左右間の再転送も必要で遅くなっているんだと思う。2019年のTWS Plus と apt-x Adaptive に期待している。

SBH90C は apt-x に対応しているのと、ネックバンド型だから左右転送の遅延もなく、きっと無線も安定しているだろうというのでチョイス。 Type-C ケーブルでデジタル接続に対応しているのもオタク心を刺激する。

感想&気になった点。

  • 専用のType-C ケーブルの太さはまだ良いが、しなやかさがイヤホンケーブルとしてはちょっと。。。ケーブルを柔らかくできないなら有線はアナログのほうが良かったと思う。
  • 有線時の遅延はスクフェスプレイに問題が無いレベル。
  • イヤホンジャック利用時はスマホの横画面でどちらを上にしたほうがスクフェスプレイがやりやすいかがデフォルトの横画面の向きと逆で、「回転」をOn/Offする必要があった。 Type-C ジャックは中央にあるので画面表示に合わせてスマホを持てて楽。
  • 無線の安定性は驚くほど上がった。中目黒駅での乗り換えで Liberty は壊滅状態だったが、 SBH90C は無問題。
  • 無線の遅延も動画視聴してる限りは気にならない。(音ゲーは最初から期待してないので試してないが、それ以外のゲームなら多分問題ない)
  • カナル型なのに、遮音性が驚くほど低い。「ポート」と呼ばれる穴があるせい。ノイズキャンセリングも無いので、通勤用途には厳しかった。セロテープかなにかでポートを塞いで見るつもり。
  • torne mobile が USB 接続だとエラーになる!スクフェス後にそのままアニメ見たいときや、充電したいときに困る。大人の事情だとは思うけどこういうつまらない制限でユーザビリティを下げるのはやめてほしい。

結論: 通勤用途には WI-1000X (と Type-C → アナログ変換ケーブル) のほうが向いてそう。

MySQL Connector/C の現状

MySQL 8 が GA になってたときは MySQL がどういうつもりなのかいまいち分からなかったのですが、 8.0.13 リリースを機に改めて調べてみたらこんな感じでした。

  • 従来の MySQL Connector/C は単体提供されない。サーバーをインストールしたらついてくる。
  • MySQL Connector/C++ は、client/server protocol の方のAPIC++ しか対応していない。 X Dev API の方は C 言語でも利用可能な形式になっている。

MySQL :: Download Connector/C (libmysqlclient)

ところで、Windows 版クライアントを考えたときに面白いのが MariaDB Connector/C です。 MySQL Connecotr/C が Windows 版でも OpenSSL 同梱になった等、 static link でシングルバイナリを作る使い方がほぼ不可能になったのに対して、 MariaDB Connector/C は OpenSSL を使わずに WindowsAPI を使って sha256_password や SSL 接続に対応していて、開発版では caching_sha2_password にも対応しました。

MySQL Connector/C がどうなってるのか公式のアナウンスがなかったのと、 MySQL Connector/Python が site-packages 直下に OpenSSL とか MySQL client の DLL とかをブッ込んでくる強硬手段を取ってきたので同じことをするとコンフリクトするので、 mysqlclient-pythonWindows版の wheel の提供を中断していました。

MariaDB Connector/C であれば static link で DLL ブッコミ不要の従来どおりの提供ができそうなので、 caching_sha2_password 対応版が出たらそっちに切り替えるつもりでいます。

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