静かなる名辞

pythonとプログラミングのこと


pythonで変数のswap(入れ替え)について考えて検討してみた

はじめに

 変数の入れ替えは、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にしてシーケンスアンパックで処理する。

まとめ

 うっかり「シーケンスアンパックと考えることができます」とか言えない。まあ、どのみち結果は変わらないのだが。

 ちょっとすっきりしないけど、最適化してくれてるんだね、みたいな話。