Recently Matt brought up some good points in his post Advanced Python exercises. First, the more easily addressed one:
To break up a Python program into multiple functions, just store related functions in a separate `.py` file, then in the main source use import
For example, if you have a file named `hello.py` that contains
def greeting(string): return "Hello, {}".format(string)
Then in your main source file (located in the same directory), you can import `hello.py` and all the functions will be available from the `hello` namespace:
import hello print hello.greeting("world")
Easy as .py
! (sorry)
Also in the post Matt talked about Python’s use of “try” and “except” as a means of flow control, as an example:
try: mynumber = float(line) except ValueError as e: sys.stderr.write("We have a problem: {}".format(e)) else: print "We have the number {}".format(mynumber)
He said “if complex return types are needed such that you’re throwing exceptions to communicate logic information rather than true fatal errors, your function needs to be redesigned.” which I agree with. But I disagree that Python itself relies on this technique, or encourages it, though of course individual developers may miss-use it.
Like the programs they are a part of functions should be written to do one thing and one thing well. In the preceding example the function float
converts its argument to a floating point value. The name of the function makes it very clear what it does, and it does its job well. If it can’t do it’s job, then it raises a ValueError
exception. All other built in and library functions I’ve seen work the same way. Think about the alternative without exceptions, for example, C:
double n = atof(line); printf ( "We have the number %f\n" , n); //Except if n == 0 it could be because there was no valid numeric expression in line
According to the documentation for atof
:
On success, the function returns the converted floating point number as a double value. If no valid conversion could be performed, the function returns zero (0.0). There is no standard specification on what happens when the converted value would be out of the range of representable values by a double. See strtod for a more robust cross-platform alternative when this is a possibility.
This is less than ideal. If we wanted to check if an input even contained a valid numeric string (which we usually would) we’d have to work harder. The strtod
alternative provides a means to do that, but we’d still have to do an explicit check after calling the function. Other C-style functions use a return code to indicate success or failure. It is also extremely easy to forget to check return codes, in which case the problem may only manifest itself in a crash later on, or worse, not at all, but instead just produce bad output. These types of problems are very hard to track down and debug. Using exceptions the program crashes precisely where the problem occurred unless the programmer handles that particular exception.
So to summarize: Each of your functions should have a well-defined job. They should do only one job and do it well. If they can’t do their job because of improper input, then they should raise an appropriate exception. I think following that idiom results in cleaner, easier to debug code. You could certainly still return complex data types where appropriate, but trying to incorporate success/failure information in a return type will often lead to difficult to debug errors when the programmer forgets to check the return status!