Program Exits
As we've seen, unlike C there is no "main" function in Python -- when we run a program, we simply execute all the code in the top-level file, from top to bottom (i.e., in the filename we listed in the command line, clicked in a file explorer, and so on). Scripts normally exit when Python falls off the end of the file, but we may also call for program exit explicitly with the built-in sys.exit function:
>>> sys.exit( ) # else exits on end of script
Interestingly, this call really just raises the built-in SystemExit exception. Because of this, we can catch it as usual to intercept early exits and perform cleanup activities; if uncaught, the interpreter exits as usual. For instance:
C:...PP2ESystem>python >>> import sys >>> try: ... sys.exit( ) # see also: os._exit, Tk( ).quit( ) ... except SystemExit: ... print 'ignoring exit' ... ignoring exit >>>
In fact, explicitly raising the built-in SystemExit exception with a Python raise statement is equivalent to calling sys.exit. More realistically, a try block would catch the exit exception raised elsewhere in a program; the script in Example 3-11 exits from within a processing function.
Example 3-11. PP2ESystemExits estexit_sys.py
def later( ): import sys print 'Bye sys world' sys.exit(42) print 'Never reached' if __name__ == '__main__': later( )
Running this program as a script causes it to exit before the interpreter falls off the end of the file. But because sys.exit raises a Python exception, importers of its function can trap and override its exit exception, or specify a finally cleanup block to be run during program exit processing:
C:...PP2ESystemExits>python testexit_sys.py Bye sys world C:...PP2ESystemExits>python >>> from testexit_sys import later >>> try: ... later( ) ... except SystemExit: ... print 'Ignored...' ... Bye sys world Ignored... >>> try: ... later( ) ... finally: ... print 'Cleanup' ... Bye sys world Cleanup C:...PP2ESystemExits>
3.4.1 os Module Exits
It's possible to exit Python in other ways too. For instance, within a forked child process on Unix we typically call the os._exit function instead of sys.exit, threads may exit with a thread.exit call, and Tkinter GUI applications often end by calling something named Tk( ).quit( ). We'll meet the Tkinter module later in this book, but os and thread exits merit a look here. When os._exit is called, the calling process exits immediately rather than raising an exception that could be trapped and ignored, as shown in Example 3-12.
Example 3-12. PP2ESystemExits estexit_os.py
def outahere( ): import os print 'Bye os world' os._exit(99) print 'Never reached' if __name__ == '__main__': outahere( )
Unlike sys.exit, os._exit is immune to both try/except and try/finally interception:
C:...PP2ESystemExits>python testexit_os.py Bye os world C:...PP2ESystemExits>python >>> from testexit_os import outahere >>> try: ... outahere( ) ... except: ... print 'Ignored' ... Bye os world C:...PP2ESystemExits>python >>> from testexit_os import outahere >>> try: ... outahere( ) ... finally: ... print 'Cleanup' ... Bye os world
3.4.2 Exit Status Codes
Both the sys and os exit calls we just met accept an argument that denotes the exit status code of the process (it's optional in the sys call, but required by os). After exit, this code may be interrogated in shells, and by programs that ran the script as a child process. On Linux, we ask for the "status" shell variable's value to fetch the last program's exit status; by convention a nonzero status generally indicates some sort of problem occurred:
[mark@toy]$ python testexit_sys.py Bye sys world [mark@toy]$ echo $status 42 [mark@toy]$ python testexit_os.py Bye os world [mark@toy]$ echo $status 99
In a chain of command-line programs, exit statuses could be checked along the way as a simple form of cross-program communication. We can also grab hold of the exit status of a program run by another script. When launching shell commands, it's provided as the return value of an os.system call, and the return value of the close method of an os.popen object; when forking programs, the exit status is available through the os.wait and os.waitpid calls in a parent process. Let's look at the shell commands case first:
[mark@toy]$ python >>> import os >>> pipe = os.popen('python testexit_sys.py') >>> pipe.read( ) 'Bye sys world 12' >>> stat = pipe.close( ) # returns exit code >>> stat 10752 >>> hex(stat) '0x2a00' >>> stat >> 8 42 >>> pipe = os.popen('python testexit_os.py') >>> stat = pipe.close( ) >>> stat, stat >> 8 (25344, 99)
When using os.popen, the exit status is actually packed into specific bit positions of the return value, for reasons we won't go into here; it's really there, but we need to shift the result right by eight bits to see it. Commands run with os.system send their statuses back through the Python library call:
>>> import os >>> for prog in ('testexit_sys.py', 'testexit_os.py'): ... stat = os.system('python ' + prog) ... print prog, stat, stat >> 8 ... Bye sys world testexit_sys.py 10752 42 Bye os world testexit_os.py 25344 99
|
To learn how to get the exit status from forked processes, let's write a simple forking program: the script in Example 3-13 forks child processes and prints child process exit statuses returned by os.wait calls in the parent, until a "q" is typed at the console.
Example 3-13. PP2ESystemExits estexit_fork.py
############################################################ # fork child processes to watch exit status with os.wait; # fork works on Linux but not Windows as of Python 1.5.2; # note: spawned threads share globals, but each forked # process has its own copy of them--exitstat always the # same here but will vary if we start threads instead; ############################################################ import os exitstat = 0 def child( ): # could os.exit a script here global exitstat # change this process's global exitstat = exitstat + 1 # exit status to parent's wait print 'Hello from child', os.getpid( ), exitstat os._exit(exitstat) print 'never reached' def parent( ): while 1: newpid = os.fork( ) # start a new copy of process if newpid == 0: # if in copy, run child logic child( ) # loop until 'q' console input else: pid, status = os.wait( ) print 'Parent got', pid, status, (status >> 8) if raw_input( ) == 'q': break parent( )
Running this program on Linux (remember, fork also didn't work on Windows as I wrote the second edition of this book) produces the following results:
[mark@toy]$ python testexit_fork.py Hello from child 723 1 Parent got 723 256 1 Hello from child 724 1 Parent got 724 256 1 Hello from child 725 1 Parent got 725 256 1 q
If you study this output closely, you'll notice that the exit status (the last number printed) is always the same -- the number 1. Because forked processes begin life as copies of the process that created them, they also have copies of global memory. Because of that, each forked child gets and changes its own exitstat global variable, without changing any other process's copy of this variable.
3.4.3 Thread Exits
In contrast, threads run in parallel within the same process and share global memory. Each thread in Example 3-14 changes the single shared global variable exitstat.
Example 3-14. PP2ESystemExits estexit_thread.py
############################################################ # spawn threads to watch shared global memory change; # threads normally exit when the function they run returns, # but thread.exit( ) can be called to exit calling thread; # thread.exit is the same as sys.exit and raising SystemExit; # threads communicate with possibly locked global vars; ############################################################ import thread exitstat = 0 def child( ): global exitstat # process global names exitstat = exitstat + 1 # shared by all threads threadid = thread.get_ident( ) print 'Hello from child', threadid, exitstat thread.exit( ) print 'never reached' def parent( ): while 1: thread.start_new_thread(child, ( )) if raw_input( ) == 'q': break parent( )
Here is this script in action on Linux; the global exitstat is changed by each thread, because threads share global memory within the process. In fact, this is often how threads communicate in general -- rather than exit status codes, threads assign module-level globals to signal conditions (and use thread module locks to synchronize access to shared globals if needed):
[mark@toy]$ /usr/bin/python testexit_thread.py Hello from child 1026 1 Hello from child 2050 2 Hello from child 3074 3 q
Unlike forks, threads run on Windows today too; this program works the same there, but thread identifiers differ -- they are arbitrary but unique among active threads, and so may be used as dictionary keys to keep per-thread information:
C:...PP2ESystemExits>python testexit_thread.py Hello from child -587879 1 Hello from child -587879 2 Hello from child -587879 3 q
Speaking of exits, a thread normally exits silently when the function it runs returns, and the function return value is ignored. Optionally, the thread.exit function can be called to terminate the calling thread explicitly. This call works almost exactly like sys.exit (but takes no return status argument), and works by raising a SystemExit exception in the calling thread. Because of that, a thread can also prematurely end by calling sys.exit, or directly raising SystemExit. Be sure to not call os._exit within a thread function, though -- doing so hangs the entire process on my Linux system, and kills every thread in the process on Windows!
When used well, exit status can be used to implement error-detection and simple communication protocols in systems composed of command-line scripts. But having said that, I should underscore that most scripts do simply fall off the end of the source to exit, and most thread functions simply return; explicit exit calls are generally employed for exceptional conditions only.