LoginSignup
13
11

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-03-04

$1 < 2 < 3$を表現するときに一般的なプログラミング言語では1 < 2 && 2 < 3と書く。

JavaScript等で1 < 2 < 3と書いても動いてるように見えるが間違い。
これは1 < 2が評価されてtrueになり、暗黙の型変換で1になり、1 < 3trueとなっているだけ。
なので、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

13
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
11