はじめに
変数の入れ替えは、C言語の教科書などにも書いてある古典的な話題です。
一番古典的な方法では、こうやります。
>>> a = 10 >>> b = 20 >>> a 10 >>> b 20 >>> tmp = a >>> a = b >>> b = tmp >>> a 20 >>> b 10
ただ、このコードは……あまり書きたくないですね。
pythonではエレガントに書ける
pythonの代入文にはイカれた機能があり、こういうものを綺麗に書けます。
>>> a = 10 >>> b = 20 >>> b,a = a,b >>> a 20 >>> b 10
参考:
タプル+シーケンスアンパックとして解釈できるか?
この記事の本題なのですが、これはタプル+シーケンスアンパックとして解釈できるのでしょうか?
構文的に考えると、まず右辺はtupleを作っているはずです。
なお、タプルを作るのはカンマであり、丸括弧ではありません。丸括弧は省略可能ですが、空のタプルの場合や構文上の曖昧さを避けるのに必要な時は例外です。例えば、 f(a, b, c) は三引数の関数呼び出しですが、f( (a, b, c) ) は 3-タプルを唯一の引数とする関数の呼び出しです。
4. 組み込み型 — Python 3.6.5 ドキュメント
左辺はシーケンスアンパックとして解釈できそうです。
ということで、これはtuple+sequence unpackなはずなので、バイトコードを読んでみましょう。
>>> def f(): ... a,b = b,a ... >>> import dis >>> dis.dis(f) 2 0 LOAD_FAST 0 (b) 3 LOAD_FAST 1 (a) 6 ROT_TWO 7 STORE_FAST 1 (a) 10 STORE_FAST 0 (b) 13 LOAD_CONST 0 (None) 16 RETURN_VALUE
解説はここに。なお、言うまでもなくCPython前提です。また、私が2018年9月現在使っている処理系はpython3.5.1です(いい加減更新しろよ・・・)。
32.12. dis — Python バイトコードの逆アセンブラ — Python 3.6.5 ドキュメント
関係あるものだけ引用します。
- LOAD_FAST
ローカルな co_varnames[var_num] への参照をスタックにプッシュします。
- STORE_FAST
TOS をローカルな co_varnames[var_num] の中に保存します
- ROT_TWO
スタックの先頭の 2 つの要素を入れ替えます。
あれ、もしかしてtuple+sequence unpackではない?
検証
まず、こうしてみる。
>>> def f(): ... a,b = (b,a) ... >>> dis.dis(f) 2 0 LOAD_FAST 0 (b) 3 LOAD_FAST 1 (a) 6 ROT_TWO 7 STORE_FAST 1 (a) 10 STORE_FAST 0 (b) 13 LOAD_CONST 0 (None) 16 RETURN_VALUE
あまり関係なさそう。
入れ替える個数を3つにしてみる。
>>> def f(): ... a,b,c = c,b,a ... >>> dis.dis(f) 2 0 LOAD_FAST 0 (c) 3 LOAD_FAST 1 (b) 6 LOAD_FAST 2 (a) 9 ROT_THREE 10 ROT_TWO 11 STORE_FAST 2 (a) 14 STORE_FAST 1 (b) 17 STORE_FAST 0 (c) 20 LOAD_CONST 0 (None) 23 RETURN_VALUE
なんかROT_THREEまで出てきて凄まじいけど、タプル生成→シーケンスアンパックされている様子はない。
4つに。
>>> def f(): ... a,b,c,d = d,c,b,a ... >>> dis.dis(f) 2 0 LOAD_FAST 0 (d) 3 LOAD_FAST 1 (c) 6 LOAD_FAST 2 (b) 9 LOAD_FAST 3 (a) 12 BUILD_TUPLE 4 15 UNPACK_SEQUENCE 4 18 STORE_FAST 3 (a) 21 STORE_FAST 2 (b) 24 STORE_FAST 1 (c) 27 STORE_FAST 0 (d) 30 LOAD_CONST 0 (None) 33 RETURN_VALUE
やっと狙ったものが出てきました。BUILD_TUPLE, UNPACK_SEQUENCEが該当する命令です。
考察
tupleオブジェクトの生成はそれなりにオーバーヘッドを伴うはずである。
CPythonインタプリタはこのようなswap代入に対して、3つまではスタックを用いた高速な処理を行うよう最適化されている。
4つ以上ある場合は(たぶん最適化を実装するのが面倒くさいので)tupleにしてシーケンスアンパックで処理する。
まとめ
うっかり「シーケンスアンパックと考えることができます」とか言えない。まあ、どのみち結果は変わらないのだが。
ちょっとすっきりしないけど、最適化してくれてるんだね、みたいな話。