何かを書き留める何か

数学や読んだ本について書く何かです。最近は社会人として生き残りの術を学ぶ日々です。

Pythonのタプルに括弧は必要なのかどうなのか

タプル同士の比較、そう思っていた時期が僕にもありました。

Pythonで式True, True, True == (True, True, True) がどう評価されるのか、頭の中で考えてみてほしい。 そして、お手元のPythonインタプリタTrue, True, True == (True, True, True) を実行してほしい。 どうなるだろうか。

>>> True, True, True == (True, True, True)
(True, True, False)

上記の結果は、あなたが予想していた結果だろうか。 それとも、全く違う結果だっただろうか。

なぜこうなるのか

ひとまず、なぜこうなるのか、Pythonの標準ライブラリの1つであるバイトコードアセンブラdisを使って、上記の式がどのようにバイトコードに変換されるのかを調べてみる。

>>> import dis
>>> dis.dis("True, True, True == (True, True, True)")
 1            0 LOAD_CONST               0 (True)
              2 LOAD_CONST               0 (True)
              4 LOAD_CONST               0 (True)
              6 LOAD_CONST               1 ((True, True, True))
              8 COMPARE_OP               2 (==)
             10 BUILD_TUPLE              3
             12 RETURN_VALUE

disのドキュメントに沿ってこのコードがどのような処理が行われるのかを追ってみる。

  1. スタックに Trueをプッシュする [True]
  2. スタックに Trueをプッシュする [True, True]
  3. スタックに Trueをプッシュする [True, True, True]
  4. スタックに (True, True, True)をプッシュする [True, True, True, (True, True, True)]
  5. スタックの先頭2つを等価比較して、結果をプッシュする
    1. True == (True, True, True) の結果はFalseである
    2. 結果のFalse をスタックにプッシュする [True, True, False]
  6. スタックから3つ取り出してタプルを作ってスタックにプッシュする [(True, True, False)]
  7. スタックの先頭を返り値として返す (True, True, False)

上記からわかることは、タプル同士の比較ではなく、左辺の一番右のTrueと右辺のタプルの比較が行われていることがわかる。 これは、そのようにパースされたからだと思われる。

実際、 6. 式 (expression) — Python 3.8.2 ドキュメント の「演算子の優先順位」を調べると、 Binding or parenthesized expressionよりも==の優先順序が上なので、タプル同士の比較ではなく上記のような動きになることがわかる。

どうすれば防げるのか

防ぐ方法の1つはmypyによるチェックである。

例えば、 hoge.pyとして、

b = True, True, True == (True, True, True)
print(b)

であるとき、

$ mypy --strict hoge.py

とすると、

hoge.py:1: error: Non-overlapping equality check (left operand type: "Literal[True]", right operand type: "Tuple[bool, bool, bool]")
Found 1 error in 1 file (checked 1 source file)

という結果が返ってくる。 --strictで指定される引数のうち、 --strict-equalityが利いていることがわかる。