Python

「a is not None」と「not a is None」は違うのか

はじめに

PythonではaがNoneでないことを確認する書き方として、

a is not None

と英語っぽく書けます。
一方、あまりこういう書き方する人はいないと思いますが(他言語の経験があってis notの書き方を知らない人ぐらい?)「「aはNoneである」ではない」の意味で、

not a is None

とももちろん書けます。
ここでクエスチョン。この二つってどう実行されるの?

バイトコードを見てみよう

早速disりましょう。

>>> import dis
>>> dis.dis(compile('a is not None', '', 'eval'))
  1           0 LOAD_NAME                0 (a)
              2 LOAD_CONST               0 (None)
              4 COMPARE_OP               9 (is not)
              6 RETURN_VALUE
>>> dis.dis(compile('not a is None', '', 'eval'))
  1           0 LOAD_NAME                0 (a)
              2 LOAD_CONST               0 (None)
              4 COMPARE_OP               9 (is not)
              6 RETURN_VALUE

同じです。

ASTを見てみよう

生成されるバイトコードは同じでした。でもどうにも納得がいきません。構文的には違うものなはずです。

さて、Pythonの標準モジュールにはastというものもあります。これを使うとPythonプログラムがどう構文解析されたかが確認できます。
parseでASTのノードになるので、さらにそれをdumpに渡すとASTが確認できます。

>>> import ast
>>> ast.dump(ast.parse('a is not None', '', 'eval'))
"Expression(body=Compare(left=Name(id='a', ctx=Load()), ops=[IsNot()], comparators=[NameConstant(value=None)]))"
>>> ast.dump(ast.parse('not a is None', '', 'eval'))
"Expression(body=UnaryOp(op=Not(), operand=Compare(left=Name(id='a', ctx=Load()), ops=[Is()], comparators=[NameConstant(value=None)])))"

違ってるやん。

どこで書き換わっているのか

  • ASTは違う
  • バイトコードは同じ

とすると書き換えているのは上記2つの間のどこかです。この間に何をしているかと言うとバイトコードの生成です。

バイトコード生成部分を見てみよう

というわけで、Pythonソースに踏み込む。見るのはPython/compile.cのcompile_visit_expr関数のUnaryOp処理している箇所

Python/compile.c抜粋
    case UnaryOp_kind:
        VISIT(c, expr, e->v.UnaryOp.operand);
        ADDOP(c, unaryop(e->v.UnaryOp.op));
        break;

何をしているかと言うと、

  1. Unary(つまり単項演算子)のオペランドをまず処理(バイトコードを生成)。今の場合は「a is None」がバイトコード化される
  2. 単項演算(今の場合はnot)を追加

つまり、こんなバイトコードが生成されるはずです。

  1           0 LOAD_NAME                0 (a)
              2 LOAD_CONST               0 (None)
              4 COMPARE_OP               8 (is)
              6 UNARY_NOT

が、初めに見たように結果のコードはこうではありませんでした。

なお、ASTからバイトコードにどう変換されるかのガチな解説が読みたい方は私が昔Python実行一通りを読んだ記事をご参照ください(宣伝)

最適化処理を見てみよう

コード生成には続きがあります。assembleという関数でジャンプ命令の飛び先のアドレス決定などを行い本当の「バイトコード」にしているわけですが、その中で最適化が行われています。ファイルが変わってPython/peephole.cのPyCode_Optimize関数の以下の部分

Python/peephole.c抜粋
                /* not a is b -->  a is not b
                   not a in b -->  a not in b
                   not a is not b -->  a is b
                   not a not in b -->  a in b
                */
            case COMPARE_OP:
                j = get_arg(codestr, i);
                if (j < 6 || j > 9 ||
                    nextop != UNARY_NOT ||
                    !ISBASICBLOCK(blocks, op_start, i + 1))
                    break;
                codestr[i] = PACKOPARG(opcode, j^1);
                fill_nops(codestr, i + 1, nexti + 1);
                break;

コードの細かい動作はややこしいですが何やっているかは上に書いてあるコメントで一目瞭然ですね。つまり、「not a is None」と書いてもここで「a is not None」に書き換えられているわけです。
ちなみに、peepholeは「のぞき穴」という意味です。ガチに「ここはこう書き換えた方が速い!」と最適化するのではなく「ん?ここちょっと書き換えてこうした方がいいんじゃね」程度の軽い最適化が行われます。

まとめ

というわけでまとめです。

  • 「a is not None」も「not a is None」も生成されるバイトコードは同じ
  • ASTは違う
  • 初めに生成されるバイトコードも違う(ASTに沿って生成するので当然)
  • 最適化処理により「not a is None」(を実行するバイトコード)が「a is not None」に書き換えられている