IPython環境で呼び出したスクリプト内でquit()exit()を使うとNameErrorが発生する。

NameError: name 'quit' is not defined
or
NameError: name 'exit' is not defined

この場合はimport sysをしてからsys.exit()を使うと想定通りの動作になる。quit()を使っているということはそこでスクリプトを終わらせたいわけで、例外でコケて止まるので一応目的は達成されているといえるがIPythonの詳細すぎるエラーメッセージがうるさい。

終了系の組み込み関数/例外が使えない

  • quit()
  • exit()

IPythonそのものがPythonプログラムであるのでスクリプトからこれらの関数や例外をそのまま使われるとIPythonのシェルそのものが終わってしまう。なのでこれらはスクリプトの名前空間からは取り除かれている。

簡単な検証

まず次のスクリプトを普通のpythonコマンドで動かす。

print(type(quit),str(quit))
print(type(exit),str(exit))
print(type(SystemExit),str(SystemExit))

結果は次のようになる。

<class '_sitebuiltins.Quitter'> Use quit() or Ctrl-Z plus Return to exit
<class '_sitebuiltins.Quitter'> Use exit() or Ctrl-Z plus Return to exit
<class 'type'> <class 'SystemExit'>

Quitterというビルトインによって終了機能が実現されていることがわかる。このQuitterのソースコードを調べてみると中では最終的に組み込み例外のSytemExitによってプログラムを終了していることがわかる。pythonコマンドからの実行ではこの例外に直接アクセスしてプログラムを止めることも可能だ。

普通のpythonコマンドもインタラクティブシェルを備えておりそこから同じプログラムを動かしても結果は同じである。

>>> type(quit), str(quit)
(<class '_sitebuiltins.Quitter'>, 'Use quit() or Ctrl-Z plus Return to exit')
>>> type(exit), str(exit)
(<class '_sitebuiltins.Quitter'>, 'Use exit() or Ctrl-Z plus Return to exit')
>>> type(SystemExit), str(SystemExit)
(<class 'type'>, "<class 'SystemExit'>")
>>> raise SystemExit(0)
# ここで正しく終了

以上をIPython内のrunコマンドから行ってみる。順番は異なるがまずインタラクティブシェルから同じコードを実行すると次の結果が返ってくる。

In [1]: type(quit), str(quit)
Out[1]: 
(IPython.core.autocall.ExitAutocall,
 '<IPython.core.autocall.ExitAutocall object at 0x00000199738E7700>')

In [2]: type(exit), str(exit)
Out[2]: 
(IPython.core.autocall.ExitAutocall,
 '<IPython.core.autocall.ExitAutocall object at 0x00000199738E7700>')

In [3]: type(SystemExit), str(SystemExit)
Out[3]: (type, "<class 'SystemExit'>")

In [4]: raise SystemExit(0)
An exception has occurred, use %tb to see the full traceback.

SystemExit: 0

c:\users\xxx\appdata\local\programs\python\python38\lib\site-packages\IPython\core\interactiveshell.py:3339: UserWarning: To exit: use 'exit', 'quit', or Ctrl-D.
  warn("To exit: use 'exit', 'quit', or Ctrl-D.", stacklevel=1)

これらの名前は存在しているもののタイプが異なっている。quitexitはIPython内のディレクティブとして上書きされていることが読み取れるがSystemExitはそのまま残っているように見える。しかしこれを直接呼び出そうとすると警告がでてごらんのようにquitexitCtrl-Dでシェルを終了させなさいと怒られる。

これをrun script.pyによって実行させるとこのようになる。

In [1]: run script.py
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
~\Documents\Python\20200401labexp\e01mmpatern.py in <module> 
----> 1 print(type(quit),str(quit))
      2 print(type(exit),str(exit))
      3 print(type(SystemExit),str(SystemExit))

NameError: name 'quit' is not defined

名前が定義されていないといわれる。runから実行したスクリプトの中では名前空間からquitexitが取り除かれているようだ。最後にスクリプト内でraise SystemExit(0)で例外を送出してみるとどうなるだろうか。エラーなしで終了するのである。

sys.exit() かraise SystemExit

以上で結論は出た。IPython内から実行したスクリプトをquit()exit()と根本的に同じ方法で中断させるには以下の2種類のいずれかを行えばよい。

import sys
sys.exit()
# or 
raise SystemExit

インポート文が邪魔だがすっきりするのはsys.exit()だろう。この中ではここにある通り結局SystemExitが送出されるので上記はプログラム中断の方法としては根本的に同じである。

感想

IPythonのシェルから直接raise SystemExitをされることまで想定してwarningが用意されているのはやられた。