Pythonで「a < b < c」と書けるのはどういう仕組みなのか調べたかった

$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モジュール。

```python
>>> 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 は スタックに残されます。<br>そうでない (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?](https://stackoverflow.com/questions/28754726/how-do-chained-comparisons-in-python-actually-work)
[Python 言語リファレンス > 4.3. 比較](https://docs.python.jp/3/library/stdtypes.html#comparisons)
[Python 標準ライブラリ > 6.10. 比較](https://docs.python.jp/3/reference/expressions.html?highlight=%E6%BC%94%E7%AE%97%E5%AD%90#comparisons)
[Python 標準ライブラリ > 32.12. dis — Python バイトコードの逆アセンブラ](https://docs.python.jp/3.6/library/dis.html)

CPythonの実装はこのへんのはず
[cpython/compile.c #L2248-L2257](https://github.com/python/cpython/blob/520b7ae27e39d1c77ea74ccd1b184d7cb43f9dcb/Python/compile.c#L2248-L2257)