(For beginners)Handling exceptions is a good habit, but if you write try-except trees for all cases, these constructions start repeating, and the code becomes complicated and hard to read. And also to write all the processing of prosot laziness.
In order for exception handling not to be painful, and to reduce the repeatability of the code, you need to use the power and flexibility of Python.
')
Introduction
In general, exceptions to the program sooner or later arise even if you follow the precautions and check everything in advance.
For example, a service script that does several actions (take from one folder, copy to another, create a third, link the file into the fourth). You can make it smart, so that it checks everything in advance: whether there are files, whether there is access to them, etc., and if something is wrong, I told everything to the user and did nothing. But this does not guarantee at all that the action itself will be executed: that after checking the disk will not be full, will not be disconnected, or the file will not be erased. Sooner or later such a situation will arise, and it is better that the program be able to adequately leave it.
Correct error message is always better than tracing. Take a look:
import os os .listdir( dir )
import os os .listdir( dir )
Traceback (most recent call last):
File "/tmp/test.py", line 12, in <module>
os.listdir (dir)
OSError: [Errno 13] Permission denied: '/ root'
The user sees the incomprehensible word traceback and a bunch of unnecessary information (and if the chain is called N several functions, the trace will be N times longer).
In principle, you can enclose the entire program in try, catch the exception, and write something:
- import os , shutil , sys
- try :
- os .listdir ( '/ etc' )
- shutil . copy ( '/home/culebron/test.txt' , '/ etc' )
- os .listdir ( '/ root' )
- f = open ( '/ etc / hosts' , 'a' )
- f.write ( ')
- except (OSError, IOError):
- print ' Error # {1 [1] [0]}: {1 [1] [1]}'. format (dir, sys.exc_info ())
Error # 13: Permission denied
(Note for beginners: after except, it is highly desirable to specify the type of exception in order to intercept only the provided type and not suppress the rest.)Now the message is readable, but what and where has broken is not clear, but if the message were accurate (for example, “It is impossible to read the folder / root”), a competent user would understand what was the matter.
Homework, which was mentioned above: make with the help of code trees so that it is precisely clear what action did not work and what error occurred. For example, so (language does not matter):
Unable to read / root folder. Mistake number 13: Permission denied
Simplest standard handler
Here is a simple script, more like a useful program. He writes a new line to the / etc / hosts file, and then displays its contents:
- #! / usr / bin / python
- myfile = '/ etc / hosts'
- try :
- with open (myfile, 'a' ) as f:
- f.write ( '127.0.0.1 anotherhost' )
- except ( OSError , IOError ):
- sys .exit ( 'Error when working with .format file (args [], sys.exc_info () [1] .args))
- try:
- with open (myfile, ' a ') as f:
- print ' ' .join (f)
- except (OSError, IOError):
- sys.exit (' Error when working with .format file (args [], sys .exc_info () [ 1 ] .args))
Obviously, the code is repeated, and much of it can be brought into the general method. Let's create a module in which we will write down all the methods for automatic exception handling. Here I give examples only for training purposes, for a console script that simply stops in case of failure.
- #! / usr / bin / python
- import sys
- def fopen ( * args):
- try :
- return open ( * args)
- except ( OSError , IOError ):
- sys .exit ( 'Error opening .format file (args [], sys.exc_info () [1] .args))
Save the file as safe and load it from the working script:
- #! / usr / bin / python
- import safe
- myfile = '/ etc / hosts'
- with safe.fopen (myfile, 'a' ) as f:
- a.write ( '127.0.0.1 anotherhost' )
- with safe.fopen (myfile, 'r' ) as f:
- print '' .join (f)
Now the script will beautifully stop if you execute it without sudo. Of course, if the failure occurs already during the writing or reading, it will not be intercepted, but later we will make protection for such cases.
Methods for wider use
So, we replaced the open function with our own, but in real problems we will need to work with a dozen more functions -
os.listdir, os.chmod, os.chown, shutil.copy, shutil.mv, shutil.mkdir, shutil.makedirs, and others. They need their own and understandable error messages.
There are 2 solutions:
make a method that will handle exceptions and call the specified method
replace methods (i.e. decorate existing ones)
From simple to complex: first try the first method, the method that calls the given one.
In the safe.py module, create a method
- def catch (method, args, message, exceptions = ( OSError , IOError )):
- if not isinstance (args, ( list , tuple )):
- args = [args]
- try :
- return method ( * args)
- except exceptions :
- quit (message.format ( * args) + '.format (sys.exc_info () [1] .args), 1)
Now you can call any functions and handle errors, indicating only an error message in place:
- safe.catch ( os .listdir, '/ root' , 'Unable to open {0}' )
- safe.catch ( shutil . copy , (pathA, pathB), 'Cannot copy {0} to {1}' )
This view is applicable, but less readable, because the action itself (os.listdir) is not at the beginning of the line, but in brackets.
Let's try the second way - to replace the method, that is, to decorate. We will need the first method, catch, we will rewrite it a little, so that we can call functions with key arguments, and create a decorator that will make a wrapper calling catch:
- def catch (method, message, exceptions , * args, ** kwargs):
- if not isinstance (message, str ):
- raise TypeError ( 'Message text must be a string' )
- try :
- return method ( * args)
- except exceptions :
- sys .exit (message.format ( * args) + '+' , '.join (sys.exc_info () [1] .args), 1)
- def wrap (method, message, exceptions = (IOError, OSError)):
- def fn (* args, ** kwargs):
- return catch (method, message, exceptions, * args, ** kwargs)
- return fn
- open = wrap (open, ' Can)
Now in the safe module there will be an open method replacing the standard open, and other methods can be wrapped in wrap. This method can also be used as a decorator for eigenfunctions.
Often, when you write service scripts, it turns out that a piece of one script needs to be used in another. Therefore, it is better to immediately count on reusing them as modules, and at least to take out all the working actions in the method, and leave the service actions (for example, the processing of command line parameters) in the global code:
- #! / usr / bin / python
- import os , shutil , safe, sys
- hosts_file = '/ etc / hosts'
- @ safe.wrap ( 'Error reading file {0}' )
- def writehost (ip, host):
- with safe. open (hosts_file, 'a' ) as f:
- f.write ( '.format (ip, host))
- if __name__ == ' __main__ ':
- writehost (sys.argv [1:])
- with safe.open (hosts_file, ' r ') as f:
- print f.read ()
Here, in fact, there is a possible exception that is not intercepted - this is reading the file (last line). In order to process it with available means, we will have to put an action into the method, but, let's say, we don’t want to do it (a swarm of small methods is also not an option), and we will write a try-except construct. In order not to repeat the already existing code that displays the message, we will move it into the safe module into a separate dump function:
(safe.py)- def catch (method, message, exceptions , * args, ** kwargs):
- if not isinstance (message, str ):
- raise TypeError ( 'Message text must be a string' )
- try :
- return method ( * args)
- except exceptions :
- quit (message, * args)
- def wrap (method, message, exceptions = ( IOError , OSError )):
- def fn ( * args, ** kwargs):
- return catch (method, message, exceptions , * args, ** kwargs)
- return fn
- open = wrap ( open , 'Can)
- def quit (msg, * args):
- sys.exit (msg.format (* args) + ' Error: ', ' , ' .join (map (str, sys.exc_info () [1] .args)), 1)
- # not all arguments are strings, they need to be converted by str
(main program)- ...
- with safe. open (hosts_file, 'r' ) as f:
- try :
- print '' .join (f)
- except IOError :
- safe.quit ( 'File {0} opened, but failed while reading.' , hosts_file)
- ...
(Note: in my programs and in my safe module, I use decorators that check the data type.)What's next?
First, the exception trace contains a lot of useful information, much more than the one printed by Python. It will be useful to make a method that saves this data somewhere in a file so that the user can send it to the developer.
Summary
Now for many typical cases, we have 4 ways to handle exceptions:
take actions to a new method and decorate it with the wrap method
decorate built-in methods with wrap too
call any method through the catch exception handler
write your try-except construct in the code and call the quit method in it
... and their labor costs are minimal: just write the name of the processor and the text of the error message. Atypical exceptions will be quite rare, and for them we will write try-except, which does not complicate the code.