I l@ve RuBoard Previous Section Next Section

3.4 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:\...\PP2E\System>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. PP2E\System\Exits\testexit_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:\...\PP2E\System\Exits>python testexit_sys.py
Bye sys world

C:\...\PP2E\System\Exits>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:\...\PP2E\System\Exits>

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. PP2E\System\Exits\testexit_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:\...\PP2E\System\Exits>python testexit_os.py
Bye os world

C:\...\PP2E\System\Exits>python
>>> from testexit_os import outahere
>>> try:
...     outahere(  )
... except:
...     print 'Ignored'
...
Bye os world

C:\...\PP2E\System\Exits>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\012'
>>> 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

Unfortunately, neither the popen nor system interfaces for fetching exit statuses worked reliably on Windows as I wrote this. Moreover, fork isn't supported at all, and popen in Python 1.5.2 and earlier fails in applications that create windows (though it works in code run from DOS console command lines, and works better in general in 2.0). On Windows:

>>> import os 
>>> stat = os.system('python testexit_sys.py')
Bye sys world
>>> print stat
0
>>> pipe = os.popen('python testexit_sys.py')
>>> print pipe.read(), 
Bye sys world 
>>> print pipe.close( )
None
>>> os.fork
Traceback (innermost last): 
 File"<stdin>", line 1, in ?
AttributeError: fork

For now, you may need to utilize Windows-specific tools to accomplish such goals (e.g., os.spawnv, and running a DOS start command with os.system ; see later in this chapter). Be sure to watch for changes on this front, though; Python 2.0 fixes Windows popen problems, and ActiveState, a company that created a fork call for Perl on Windows, has begun focusing on Python tools development.

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. PP2E\System\Exits\testexit_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. PP2E\System\Exits\testexit_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:\...\PP2E\System\Exits>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.

    I l@ve RuBoard Previous Section Next Section