$1 < 2 < 3$を表現するときに一般的なプログラミング言語では1 < 2 && 2 < 3
と書く。
JavaScript等で1 < 2 < 3
と書いても動いてるように見えるが間違い。
これは1 < 2
が評価されてtrue
になり、暗黙の型変換で1
になり、1 < 3
でtrue
となっているだけ。
なので、2 < 3 > 1
などとキモく書くと1 > 1
でfalseになる。
だけどPythonでは比較演算子をいくつでも連鎖できる。
1 < 2 < 3
も、 2 < 3 > 1
も、きちんとTrueになる。
これがどう動いてるのかずっと不思議だったので調べた。
動作確認: Python 3.6, macOS 10.13.3
disモジュール
disモジュールを使うと、Pythonのコードを逆アセンブルできる。
disassembleだからdisモジュール。
>>> from dis import dis
>>> dis("555 < 666 < 777 < 333")
1 0 LOAD_CONST 0 (555)
2 LOAD_CONST 1 (666)
4 DUP_TOP
6 ROT_THREE
8 COMPARE_OP 0 (<)
10 JUMP_IF_FALSE_OR_POP 28
12 LOAD_CONST 2 (777)
14 DUP_TOP
16 ROT_THREE
18 COMPARE_OP 0 (<)
20 JUMP_IF_FALSE_OR_POP 28
22 LOAD_CONST 3 (333)
24 COMPARE_OP 0 (<)
26 RETURN_VALUE
>> 28 ROT_TWO
30 POP_TOP
32 RETURN_VALUE
普通は関数を渡すと思うけど今回は短いので文字列で渡した。
命令一覧
出てきた命令だけ公式ドキュメントから抜粋した。
バイトコード命令 | 説明 |
---|---|
LOAD_CONST(consti) |
co_consts[consti] をスタックにプッシュします。 |
POP_TOP | スタックの先頭 (TOS: Top Of Stack) の要素を取り除きます。 |
DUP_TOP | スタックの先頭にある参照の複製を作ります。 |
ROT_TWO | スタックの先頭の 2 つの要素を入れ替えます。 |
ROT_THREE | スタックの二番目と三番目の要素の位置を 1 つ上げ、先頭を三番目へ下げます。 |
COMPARE_OP(opname) | ブール命令を実行します。命令名は cmp_op[opname] にあります。 |
JUMP_IF_FALSE_OR_POP(target) | TOS が偽ならば、バイトコードカウンタを target に設定し、TOS は スタックに残されます。 そうでない (TOS が真) なら、TOS はポップされます。 |
RETURN_VALUE | 関数の呼び出し元へ TOS を返します。 |
スタックの動き
555 < 666 < 777 < 333
のスタックの動きはこうなる。はず。
「先頭」はこの表では右。表記を合わせるためTOSも「先頭」とした。ゲシュタルト崩壊勘弁。
|命令|スタック1|スタック2|スタック3|スタック4|説明
---|---|---|---|---|---|---
0|LOAD_CONST 0 (555)|555|-|-|-|co_const[0]
(555)を積む
2|LOAD_CONST 1 (666)|555|666|-|-|co_const[1]
(666)を積む
4|DUP_TOP|555|666|666|-|先頭を複製
6|ROT_THREE|666|555|666|-|先頭3つの要素を入れ替え
8|COMPARE_OP 0 (<)|666|True|-|-|先頭2つを比較
10|JUMP_IF_FALSE_OR_POP 28|666|-|-|-|先頭がTrueなら除去、Falseなら28に飛ぶ
12|LOAD_CONST 2 (777)|666|777|-|-|co_const[2]
(777)を積む
14|DUP_TOP|666|777|777|-|先頭を複製
16|ROT_THREE|777|666|777|-|先頭3つの要素を入れ替え
18|COMPARE_OP 0 (<)|777|True|-|-|先頭2つを比較
20|JUMP_IF_FALSE_OR_POP 28|777|-|-|-|先頭がTrueなら除去、Falseなら28に飛ぶ
22|LOAD_CONST 3 (333)|777|333|-|-|co_const[3]
(333)を積む
24|COMPARE_OP 0 (<)|False|-|-|-|先頭2つを比較
26|RETURN_VALUE|-|-|-|-|先頭を返して終了
28|ROT_TWO|-|-|-|-|先頭2つを入れ替え
30|POP_TOP|-|-|-|-|先頭を除去
32|RETURN_VALUE|-|-|-|-|先頭を返して終了
比較演算子<
<=
>
>=
==
!=
is
is not
in
not in
が連続してたら最後の2つになるまで数値同士を比較、途中でFalseになったらその時点で中断、最後の2つになったらそのTrue/Falseを返すようになってる。(ドキュメント通りだから当たり前)
何か調べたかったことと違う気がするけど探究心が枯れた
参考
How do chained comparisons in Python actually work?
Python 言語リファレンス > 4.3. 比較
Python 標準ライブラリ > 6.10. 比較
Python 標準ライブラリ > 32.12. dis — Python バイトコードの逆アセンブラ
CPythonの実装はこのへんのはず
cpython/compile.c #L2248-L2257