注意:この記事は議論中の機能について紹介し自分の考えを述べるものです。 Python 3.8 で追加されるらしいよ!みたいな拡散はしないでください。
Python-ideas で dict + dict が提案され、PEPになった。
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)
とかのメソッドを足すだけで我慢してほしい。