(Emacs) python-shell-send-bufferではまった

筆者はPythonの短いプログラムを書く時によくEmacsのPython.elをよく使う。主な理由は長年使っていて手に馴染んでいるからであるが、何よりもプログラムの編集中にカーソルを移動させることなく都度C-c C-cで実行結果を別バッファで見れるのが便利であり、筆者には手放せない。UNIXのシェルからpythonと打って起動する対話モードは、計算機代わりに1行書くのにはよく使うが、2〜3行程度であっても複数行のブロックを書く気になれないし、Jupyter Notebookは起動に時間がかかるし、ブラウザの操作が面倒だし、重い。Emacsはすぐに開くし、Python.elのお陰で、ファイルを開いてコードを書いて、初回はC-c C-p, C-c C-c, C-c C-zで、2回目以降はC-c C-cですぐに実行結果が見れる。

さて、Emacsのpython.elのPython modeでC-c C-cすると if __name__ == '__main__': のブロックが実行されないが、筆者はそれが __name__ == '__main__' じゃないからだと思っていた。
ある日、何か延々と試行錯誤しながらプログラムを作っている途中で、テスト用にC-c C-cで実行する時とシェルから実行する時とで動作を変えたくなって、ふと if __name__ == '__main__':else: を追加すれば実現できるのではないかと思って、やってみたら成功した。その時、たまたま if False: のブロックが if __name__ == '__main__': の直前にあり、次のような形をしていた。

#!/usr/bin/python

def foo(a, b):
    return a + b

if False:
    def foo(a, b):
        return a - b

if __name__ == '__main__':
    print(foo(1, 2))
else:
    print(foo(3, 4))

これをC-c C-cで実行すると else: 側が実行されて 7 が出力され、シェルから実行すると if __name__ == '__main__': 側が実行されて 3 が出力される。

グッドアイデアと満足してしばらくそのようにしていた後、 if False:if True: に変えると、C-c C-cした時に if __name__ == '__main__': 側のコードも else: 側のコードも実行されなくなって、頭の中が???となった。
何が悪いのだろうと思って調べ始め、 if False: に戻して else: 側に入る時に print(__name__) としてみると予想に反して __main__ と表示されるし、 if False のブロックをコメントアウトしてC-c C-cすると

    else:
       ^
SyntaxError: invalid syntax
というエラーが出て、混乱した。

if True: にした時に末尾に if __name__ == '__main__': のブロックをもう1つ足すとそちらは実行されたので、それでようやく、C-c C-cでは if __name__ == '__main__': のブロックが消されるのだと気付いた。

C-c C-cで起動されるpython-shell-send-bufferのドキュメントをM-x describe-functionで見ると、

When optional argument SEND-MAIN is non-nil, allow execution of
code inside blocks delimited by "if __name__== '__main__':".
と書いてあるだけで、どういう仕組みで"if __name__== '__main__':"のブロックを実行しないようにしているのかが書いていないが、Python.elの中身を追っていくと、途中で実行されるpython-shell-buffer-substringのドキュメントに
  1. When optional argument NOMAIN is non-nil everything under an
     "if __name__ == '__main__'" block will be removed.
と書いてあった。"if __name__== '__main__':"のブロックを消す具体的な処理を見ると、確かにpython-nav-if-name-mainが if __name__ == (['"])__main__\1: を検索し、python-nav-forward-sexpがブロックの終端まで移動しており、"if __name__== '__main__':"のelse:ブロックは消えないようになっていた。

つまり、直前に if False: ブロックがある時に if __name__ == '__main__':else: 側のコードが実行されるのは、"if __name__== '__main__':"のブロックが消されて

if False:
    def foo(a, b):
        return a - b

else:
    print(foo(3, 4))
になるからであり、 if True: に変えると実行されなくなるのも、 if False: のブロックをコメントアウトすると else:SyntaxErrorになるのも当然であった。

残念...


筆者はコード片を一時的に無効にするのを、コメントアウトするのでなく、if False:ブロックにするのをよくやる。また有効化するのを1箇所FalseをTrueに変えるだけでできるし、無効化するブロックもキーワードや変数が色分けされていてほしいからである。C言語でも、コメントアウトしたり#if 0〜#endifで括るとコンパイル対象にもならなくなり、筆者にはメンテナンスされない粗大ゴミになったように見えるので、大体if (0) {~}で括る。

まさか、そんな習慣で罠にはまるとは。