Как стать автором
Обновить

Ловля исключений с минимумом усилий

Время на прочтение 7 мин
Количество просмотров 2.1K
(Для начинающих)

Обрабатывать исключения — хорошая привычка, но если писать на все случаи деревья try-except, эти конструкции начинают повторяться, а код становится сложным и трудночитаемым. А ещё писать все обработки просот лень.

Чтобы обработка исключений не была болезненной, и чтобы уменьшить повторяемость кода, нужно пользоваться мощью и гибкостью Питона.

Введение


Вообще, исключения в программе рано или поздно возникнут даже если выполнять меры предосторожности и проверять всё заранее.

Например, служебный скрипт, делающий несколько действий (взять из одной папки, скопировать в другую, создать третью, залинковать файл в четвёртой). Можно сделать его умным, чтобы он проверил всё заранее: существуют ли файлы, есть ли доступ к ним и т.д., и если что-то не так, сообщил всё пользователю и ничего не делал. Но это вовсе не гарантирует, что само действие выполнится: что после проверки диск не заполнится, не отсоединится, или файл не сотрётся. Рано или поздно такая ситуация возникнет, и лучше чтобы программа смогла достойно выйти из неё.

Корректное сообщение об ошибке всегда лучше, чем трассировка. Взгляните:
  1. import os
  2. 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'

Пользователь видит непонятное слово traceback и кучу ненужной информации (а если по цепочке вызвано N несколько функций, трассировка будет в N раз длиннее).

В принципе, можно заключить всю программу в try, перехватить исключение и написать что-нибудь:
  1. import os, shutil, sys
  2.  
  3. try:
  4.         os.listdir('/etc')
  5.         shutil.copy('/home/culebron/test.txt', '/etc')
  6.         os.listdir('/root')
  7.         f = open('/etc/hosts', 'a')
  8.         f.write(')
  9. except (OSError, IOError):
  10.         print 'Error #{1[1][0]}: {1[1][1]}'.format(dir, sys.exc_info())



	Error #13: Permission denied


(Примечание для начинающих: после except очень желательно указывать тип исключения, чтобы перехватывать только предусмотренный тип и не подавлять остальные.)

Теперь сообщение читаемо, но что и где сломалось — непонятно, а ведь если бы сообщение было точным (напрмер, «Невозможно прочитать папку /root»), компетентный пользователь понял бы, в чём дело.

Домашнее задание, про которое говорилось выше: сделайте при помощи деревьев кода так, чтобы было точно понятно, какое действие не получилось и какая ошибка произошла. Например, так (язык не имеет значения):

	Невозможно прочитать папку /root. Ошибка №13: Permission denied


Простейший стандартный обработчик


Вот простой скрипт, более похожий на полезную программу. Он пишет новую строку в файл /etc/hosts, а затем выводит его содержимое:
  1. #!/usr/bin/python
  2.  
  3. myfile = '/etc/hosts'
  4. try:
  5. with open(myfile, 'a') as f:
  6.         f.write('127.0.0.1 anotherhost')
  7. except (OSError, IOError):
  8.         sys.exit('Ошибка при работе с файлом .format(args[], sys.exc_info()[1].args))
  9.  
  10. try:
  11.         with open(myfile, 'a') as f:
  12.         print ''.join(f)
  13. except (OSError, IOError):
  14.         sys.exit('Ошибка при работе с файлом .format(args[], sys.exc_info()[1].args))



Очевидно, что код повторяется, и многое из него можно вынести в общий метод. Создадим модуль, в котором запишем все методы для автоматической обработки исключений. Здесь я привожу примеры только в учебных целях, для консольного скрипта, который в случае сбоя просто останавливается.

  1. #!/usr/bin/python
  2. import sys
  3. def fopen(*args):
  4.         try:
  5.                 return open(*args)
  6.         except (OSError, IOError):
  7.                 sys.exit('Ошибка открытии файла .format(args[], sys.exc_info()[1].args))



Сохраним файл под именем safe и загрузим из рабочего скрипта:

  1. #!/usr/bin/python
  2. import safe
  3.  
  4. myfile = '/etc/hosts'
  5. with safe.fopen(myfile, 'a') as f:
  6.         a.write('127.0.0.1 anotherhost')
  7.  
  8. with safe.fopen(myfile, 'r') as f:
  9.         print ''.join(f)



Теперь скрипт красиво остановится, если его выполнить без sudo. Конечно, если сбой произойдёт уже во время записи или чтения, то перехвачен он не будет, но позже мы сделаем защиту и для таких случаев.

Методы для более широкого применения


Итак, мы заменили функцию open на свою, но в реальных задачах нам понадобится работать ещё с десятком функций —
os.listdir, os.chmod, os.chown, shutil.copy, shutil.mv, shutil.mkdir, shutil.makedirs и другими. Для них нужны свои и понятные сообщения об ошибке.

Есть 2 решения:

сделать метод, который будет обрабатывать исключения и вызывать указанный метод
заменить методы (то есть декорировать существующие)

От простого к сложному: сначала попробуем первый способ, метод, вызывающий заданный.

В модуле safe.py создаём метод

  1. def catch(method, args, message, exceptions = (OSError, IOError)):
  2.         if not isinstance(args, (list, tuple)):
  3.                 args = [args]
  4.         try:
  5.                 return method(*args)
  6.         except exceptions:
  7.                 quit(message.format(*args) + '.format(sys.exc_info()[1].args), 1)



Теперь можно вызывать любые функции и обрабатывать ошибки, указывая на месте только сообщение об ошибке:

  1. safe.catch(os.listdir, '/root', 'Невозможно открыть {0}')
  2. safe.catch(shutil.copy, (pathA, pathB), 'Невозможно скопировать {0} в {1}')



Такой вид применим, но менее читаем, потому что само действие (os.listdir) находится не в начале строки, а в скобках.

Попробуем второй способ — заменить метод, то есть декорировать. Первый метод, catch, нам понадобится, мы его немного перепишем, чтобы можно было вызывать функции с аргументами-ключами, и создадим декоратор, который будет делать обёртку, вызывающую catch:

  1. def catch(method, message, exceptions, *args, **kwargs):
  2.         if not isinstance(message, str):
  3.                 raise TypeError('Текст сообщения должен быть строкой')
  4.         try:
  5.                 return method(*args)
  6.         except exceptions:
  7.                 sys.exit(message.format(*args) + ' + ', '.join(sys.exc_info()[1].args), 1)
  8.  
  9. def wrap(method, message, exceptions = (IOError, OSError)):
  10.         def fn(*args, **kwargs):
  11.                 return catch(method, message, exceptions, *args, **kwargs)
  12.         return fn
  13.  
  14. open = wrap(open, 'Can)



Теперь в модуле safe будет метод open, заменяющий стандартный open, а другие методы можно будет обернуть в wrap. Этот метод можно использовать и как декоратор для собственных функций.

Часто, когда пишешь служебные скрипты, оказывается, что кусок одного скрипта нужно использовать в другом. Поэтому лучше сразу рассчитывать на то, чтобы повторно их использовать в качестве модулей, и хотя бы выносить все рабочие действия в метод, а служебные действия (например, обработку параметров командной строки) оставить в глобальном коде:

  1. #!/usr/bin/python
  2. import os, shutil, safe, sys
  3.  
  4. hosts_file = '/etc/hosts'
  5.  
  6. @safe.wrap('Ошибка при чтении файла {0}')
  7. def writehost(ip, host):
  8.         with safe.open(hosts_file, 'a') as f:
  9.                 f.write('.format(ip, host))
  10.  
  11.         if __name__ == '__main__':
  12.                 writehost(sys.argv[1:])
  13.         with safe.open(hosts_file, 'r') as f:
  14.                 print f.read()



Здесь, на самом деле, есть возможное исключение, которое не перехватывается — это чтение файла (последняя строка). Чтобы его обработать имеющимися средствами, придётся вынести действие в метод, но, допустим, мы не хотим этого сделать (рой мелких методов — тоже не выход), и напишем конструкцию try-except. Чтобы не повторять уже существующий код, который выводит сообщение, вынесем его в модуль safe в отдельную функцию dump:

(safe.py)
  1. def catch(method, message, exceptions, *args, **kwargs):
  2.         if not isinstance(message, str):
  3.                 raise TypeError('Текст сообщения должен быть строкой')
  4.         try:
  5.                 return method(*args)
  6.         except exceptions:
  7.                 quit(message, *args)
  8.  
  9. def wrap(method, message, exceptions = (IOError, OSError)):
  10.         def fn(*args, **kwargs):
  11.                 return catch(method, message, exceptions, *args, **kwargs)
  12.         return fn
  13.  
  14. open = wrap(open, 'Can)
  15.  
  16. def quit(msg, *args):
  17.         sys.exit(msg.format(*args) + 'Ошибка: ', ', '.join(map(str, sys.exc_info()[1].args)), 1)
  18.         # не все аргументы - строки, их надо преобразовать функцией str



(главная программа)
  1. ...
  2. with safe.open(hosts_file, 'r') as f:
  3. try:
  4.         print ''.join(f)
  5. except IOError:
  6.         safe.quit('Файл {0} открылся, но при чтении произошёл сбой.', hosts_file)
  7. ...


(Примечание: в своих программах и своём модуле safe я использую декораторы, проверяющие тип данных.)

Что дальше?


Во-первых, трассировка исключения содержит очень много полезной информации, намного больше, чем та, что печатается Питоном. Полезно будет сделать метод, сохраняющий эти данные куда-либо в файл, чтобы пользователь мог отправить его разработчику.

Резюме


Теперь на множество типичных случаев у нас есть 4 способа обрабатывать исключения:
вынести действия в новый метод и декорировать его методом wrap
декорировать встроенные методы тоже с помощью wrap
вызвать любой метод через обработчик исключений catch
написать в коде свою конструкцию try-except и в ней вызвать метод quit

…и трудозатраты у них минимальны: всего лишь написать имя обработчика и текст сообщения об ошибке. Нетипичные исключения будут довольно редкими, и для них мы напишем try-except, который не усложнит код.
Теги:
Хабы:
+5
Комментарии 15
Комментарии Комментарии 15

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн