Skip to main content

退出方式

这篇文章我们来讲一下 N 种从 Python 退出的姿势。那我相信大家一定会经常用到退出这个功能,就是程序运行到某个阶段,然后我就不再继续运行了,我想把整个进程退出来。我相信大部分人用的退出方式应该就是左边这四种,分别是 quit,exit,sys.exit,跟 os._exit。那你知道这四种方法之间它们有什么不同吗?

quit 与 exit 的来源​

那我们先来说前两个,这个 quit 跟 exit 几乎是一模一样的。它们两个都属于一个叫 site 的 module。那你说为什么我用的时候从来没有 import 过这个 site module 呢?因为在你正常运行 Python 的时候,它是会自动帮你把这个 site module import 进来,并且把 quit 跟 exit 这两个东西放到 builtins 里,这样你在你程序的任何一个地方运行这两个函数,它都能退出来。

我们可以看这就是它的源代码。它就是把这个 builtins.quit 跟 exit 都赋值成了这个 Quitter。就它们俩除了名字之外,剩下几乎是一模一样的。

Lib/site.py
def setquit():
"""Define new builtins 'quit' and 'exit'.

These are objects which make the interpreter exit when called.
The repr of each object contains a hint at how it works.

"""
if os.sep == '\\':
eof = 'Ctrl-Z plus Return'
else:
eof = 'Ctrl-D (i.e. EOF)'

builtins.quit = _sitebuiltins.Quitter('quit', eof)
builtins.exit = _sitebuiltins.Quitter('exit', eof)

我们可以点进这个 Quitter 看一下。它本质呢就是把 stdin close 之后,然后 raise 了一个 SystemExit。

Lib/_sitebuiltins.py
class Quitter(object):
def __init__(self, name, eof):
self.name = name
self.eof = eof
def __repr__(self):
return 'Use %s() or %s to exit' % (self.name, self.eof)
def __call__(self, code=None):
# Shells like IDLE catch the SystemExit, but listen when their
# stdin wrapper is closed.
try:
sys.stdin.close()
except:
pass
raise SystemExit(code)

sys.exit 与 SystemExit​

那我们再说这个 sys.exit 跟前两个 quit 跟 exit 原理也是一样的,它同样相当于 raise 了一个 SystemExit。我们可以看这个 sys.exit,我们可以看这是 sysmodule.c 里面 sys.exit 的 implementation,在第 820 行这里,它就相当于是 raise 了一个 SystemExit。

Python/sysmodule.c
static PyObject *
sys_exit_impl(PyObject *module, PyObject *status)
/*[clinic end generated code: output=13870986c1ab2ec0 input=b86ca9497baa94f2]*/
{
/* Raise SystemExit so callers may catch it or clean up. */
PyThreadState *tstate = _PyThreadState_GET();
_PyErr_SetObject(tstate, PyExc_SystemExit, status);
return NULL;
}

quit、exit 与 sys.exit 的区别​

好,那前三种方式,quit,exit,包括 sys.exit,它们的原理是一样的。它们有什么不同呢?这个 quit 跟 exit 我们说了几乎是一模一样的。但是它们俩有一个问题,我们之前不是说了,它们是属于 site module 的,但 Python 并不总是在运行的时候会自动 import 这个 module。我们看这种运行方式,当我们用 python -S 的时候,它就会不 import 这个 site module。这个时候 quit 跟 exit 这两个名字就不存在了。因此这两种写法,只推荐大家在 interactive interpreter 的时候用。就是当你打 Python,然后一行一行那么输命令的时候,你可以用。因为它们俩打起来比较快。在所有的比较正式的代码里,大家都要用 sys.exit。

第 1 步 · 用 python -S 运行1 / 10

加上 -S 运行时不会 import site 这个 module,quit 和 exit 这两个名字就不存在了,第 4 行直接报 NameError。

main.py
import sys
import os
quit()
exit()
sys.exit()
os._exit()

捕获 SystemExit 的问题​

好,那我们说前三者的原理,都是 raise 了一个 SystemExit exception。也就是说前三者都会被这个 try,except 给 catch 到。我们看左边的代码。我们 quit 之后呢,catch 了一个 SystemExit,然后在这个 exception 里呢,我们就 print 一个东西,我们运行一下这段代码。大家可以看到 quit 并没有起到它本身应该起的作用,就是退出程序。我们的第 12 行的 print,依然打印出来了。

第 2 步 · catch 住 SystemExit2 / 10

quit() raise 的 SystemExit 被 except SystemExit 接住了,程序没有退出,第 12 行的 print 照样打印出来。

main.py
import sys
import os
try:
quit()
exit()
sys.exit()
os._exit()
except SystemExit:
print("Ignore Exit!")
print("Yeah!")
quit()
exit()
sys.exit()
os._exit()

那么这种行为本身呢,可以说是有好有坏。好的地方呢,是 Python 允许你更灵活的处理退出这件事情。那坏的是呢,当你以为你退出了的时候,其实这个程序它可能没退出。

尤其有一些人呢,还喜欢这么写程序。像这样,它写一个空的 except。那如果你真的写程序的话,我们运行一下,你会发现它依然没有正常的退出,它还是把这个 Yeah! 给打印出来了。

第 3 步 · 空的 except3 / 10

空的 except: 什么异常都接,SystemExit 也一样被吞掉,程序还是没有退出。

main.py
import sys
import os
try:
quit()
exit()
sys.exit()
os._exit()
except:
pass
except SystemExit:
print("Ignore Exit!")
print("Yeah!")

当然了,如果你用 PyCharm,或者你用一些 VS Code 的插件的话,它这块会提醒你,不能这么写。它告诉你要写成这个形式,你要做一个 except Exception。这是最起码的。那如果写成这个形式的话,我们跑一下,可以看到第 12 行就没有打印了。因为 SystemExit 并不是 Exception 的一个 derived class。所以大家一定要记住啊,永远不要写一个空的 except:,会出很多奇怪的问题。

第 4 步 · except Exception4 / 10

SystemExit 不是 Exception 的子类,except Exception 接不住它,程序正常退出,第 12 行没有打印。

main.py
import sys
import os
try:
quit()
exit()
sys.exit()
os._exit()
except Exception:
except:
pass
print("Yeah!")

直接 raise SystemExit​

好,那我们已经知道,前面三种方法的本质都是 raise 了一个 SystemExit,对不对?那这个时候呢,我们其实就能想到一种并不是那么常用的但是跟他们三个等价的方法,就是直接 raise 这个 exception,大家看第五行,我可以直接把这个 exception raise 出来,它的效果是一样的。我运行了一下,结果跟之前一样。所以你当然也可以写这种等价形式。只是语义上说,它并没有另外三个那么明确。

第 5 步 · 直接 raise SystemExit5 / 10

第 5 行直接 raise SystemExit,效果和前三种写法一样,程序退出,什么都没有打印。

main.py
import sys
import os
try:
raise SystemExit
quit()
exit()
sys.exit()
os._exit()
except Exception:
pass
print("Yeah!")

os._exit 的系统调用​

好,那说到这儿,你肯定已经能感受到最后一个 os._exit,跟其他三个不一样了。否则我们就放到一起说了,对吧?那这个 os._exit,它是直接做了一个内核调用,做了一个 system call操作系统内核提供给程序的接口,程序通过它请求内核替自己做事,比如结束进程。它在不同的操作系统下背后原理是不同的,毕竟 system call 不一样嘛。但是在所有的系统下,它都会不管三七二十一,直接退出你的程序。

我们可以简单看一下,就是在这个 os module 里面,如果 posix 在的话,它会直接在 posix 里面,import 下划线 _exit。然后这个下划线 _exit 呢,就是一个做了一个 system call 的函数。

Lib/os.py
if 'posix' in _names:
name = 'posix'
linesep = '\n'
from posix import *
try:
from posix import _exit
__all__.append('_exit')
except ImportError:
pass
import posixpath as path

好,我们看如果直接运行这段代码的话,会发生什么?我们看它报错了,对不对?它说这个 _exit 需要一个 status argument。这是什么意思呢?就是操作系统啊,需要每一个进程在结束的时候向操作系统报告一下自己结束的情况,是否是正常的结束了,这个就是退出时候这个状态码。一般来说呢,用 0 代表一切正常,用所有的非 0 数代表是异常退出,然后用不同的值代表不同的异常。

第 6 步 · os._exit 不给状态码6 / 10

os._exit 是 system call 的接口,必须给一个 status,不给就报 TypeError。

main.py
import sys
import os
os._exit()
try:
raise SystemExit
quit()
exit()
sys.exit()
os._exit()
except Exception:
pass
print("Yeah!")

那有人可能问,为什么之前那些函数没有状态码没事?我们说过那些函数是 Python 的一个机制,对吧?当你不给它状态码的时候,它就默认是 0,正常退出。当然实际上你也可以给那些函数状态码。那对于 os._exit 来说,由于它直接就是一个 system call 的 interface,所以你必须要给它一个状态码。那一般来说,如果我们认为这是一个正常的退出的话,我们就给一个 0。这个程序长成这个样子。我们再来运行一下,可以看到它就正常退出了。

那在之前我们用那三个函数的时候,我们说我们可以 try/except,然后拿到这个 SystemExit,通过处理这个 exception 让它不退出。我们试一下在 os._exit 行不行?我们把程序写成这个样子,然后再运行一下,可以看到它依然无情的退出了,第八行并没有被打印出来。这个是 os._exit 的特点。它退出的时候是不管任何其他乱七八糟的东西的,它就直接退掉了。

第 7 步 · try/except 拦不住 os._exit7 / 10

os._exit(0) 外面包了 try/except,程序依然直接退出,第 8 行没有打印。

运行后用 echo $? 拿上一个进程的状态码,这里是 0。

main.py
import os
try:
os._exit(0)
except:
pass
print("Yeah!")
os._exit()

那同样的,跟 raise 一个 SystemExit 比,它也是有好有坏。好处是你可以保证它当下立刻马上退出。坏处呢就是你可能本来写了一些退出时候运行的一些 hook,那些代码就都运行不了了。那一般来说在你的主进程是不太推荐使用这种退出方式。

进程的退出状态码​

好,那我们既然刚才已经提到了进程结束时候的状态码,我们就看一下这个状态码。那在 Linux 包括 Mac 里呢,我们可以通过 echo $? 来拿到上一个进程结束时候的状态码。我们可以看到它退出时候的状态码是 0。我们把程序改成 os._exit(1)。然后运行一下程序,我们再尝试拿一下这个状态码,可以看到这个状态码就是 1 了。

第 8 步 · os._exit(1) 的状态码8 / 10

改成 os._exit(1),echo $? 拿到的状态码就是 1。

main.py
import os
try:
os._exit(1)
os._exit(0)
except:
pass
print("Yeah!")

那相似的不管是 quit、exit,还是我更推荐大家使用的 sys.exit,我们都可以传进去状态码。我们把这个程序运行一下,然后我们拿一下这个进程结束时候的状态码,可以看到状态码是 0。

第 9 步 · sys.exit(0) 的状态码9 / 10

sys.exit(0) 正常退出,echo $? 拿到的状态码是 0。

main.py
import sys
import os
sys.exit(0)
try:
os._exit(1)
except:
pass
print("Yeah!")

我们把 sys.exit 里面变成 1,可以看到它结束的状态码就是 1 了。

第 10 步 · sys.exit(1) 的状态码10 / 10

把 sys.exit 里面换成 1,状态码就是 1。

main.py
import sys
sys.exit(1)
sys.exit(0)

那么这个状态码呢经常被拿来判断,我自己新建的这个进程有没有正确的运行退出。所以大家不论是在用 sys.exit,或者是用 os._exit 的时候,都应该考虑清楚,我这个状态码应该给什么。是正常退出的话就要给 0,是非正常退出的话一般就给一个非 0 的数。那么很常见的是用 1 来表示有错误。

interactive shell 的 EOF 退出​

当然除了我们上面说到的很多种退出方法之外,在 interactive shell 里面还有一些退出方式,我们可以把它们统称为 EOF,就是 end of file 的退出方式。那这些退出方式呢可能比你打一个 exit() 回车还要更快一点。在 Unix 下就是 Linux 和 Mac,你都可以通过按 Ctrl+D 的方式退出。那 Python 实际上是在 parse stdin 的时候去判断这个 EOF。那如果是在 Windows 上呢你可以先按 Ctrl+Z,然后再按回车来退出 Python 的 interactive shell。

那这两者的原理呢实际上都是 Python,在专门针对 interactive shell 这个部分做的 parsing 的一些判断。它代码层级比较深也比较复杂啊,我们这一次呢就不讲了。

好,那以上就是我们这篇文章的全部内容。关于退出的各种姿势你都学会了吗?