предисловие
Это шестая статья из серии "Мастер Python".[Посмотреть все статьи цикла]
Если вы программируете на Python, вы не можете избежать исключений, потому что исключения повсюду в языке. Например, когда вы нажимаетеctrl+cexit, интерпретатор сгенерируетKeyboardInterruptаномальный. иKeyError,ValueError,TypeErrorEtc. — старый друг, которого можно увидеть повсюду в повседневном программировании.
Работа по обработке исключений состоит из двух частей: «поймать» и «выбросить». "захватить" означает использоватьtry ... exceptОберните конкретный оператор, чтобы правильно завершить процесс ошибки. и правильное использованиеraiseАктивное «генерирование» исключений — неотъемлемая часть элегантного кода.
В этой статье я поделюсь тремя полезными привычками, связанными с обработкой исключений. Прежде чем читать дальше, я надеюсь, что вы поняли следующие моменты знаний:
- Базовый синтаксис и использование исключений* (рекомендуется прочитать официальную документацию«Ошибки и исключения»)*
- Зачем использовать исключения вместо возврата ошибок * (рекомендуется прочитать"Советы, как заставить функцию возвращать результат")*
- Почему исключения поощряются при написании Python(рекомендуется к прочтению«Пишите более чистый Python: используйте исключения»)
три полезные привычки
1. Делайте только самый точный отлов исключений
Если вы недостаточно знаете о механизме аномалии, у вас неизбежно возникнет естественный страх перед ней. Вы можете подумать: * Исключения — это плохо, а хорошая программа должна перехватывать все исключения и обеспечивать бесперебойную работу. *Код, написанный с этой идеей, обычно содержит большой раздел неоднозначной логики захвата исключений.
В качестве примера возьмем исполняемый скрипт:
# -*- coding: utf-8 -*-
import requests
import re
def save_website_title(url, filename):
"""获取某个地址的网页标题,然后将其写入到文件中
:returns: 如果成功保存,返回 True,否则打印错误,返回 False
"""
try:
resp = requests.get(url)
obj = re.search(r'<title>(.*)</title>', resp.text)
if not obj:
print('save failed: title tag not found in page content')
return False
title = obj.grop(1)
with open(filename, 'w') as fp:
fp.write(title)
return True
except Exception:
print(f'save failed: unable to save title of {url} to {filename}')
return False
def main():
save_website_title('https://www.qq.com', 'qq_title.txt')
if __name__ == '__main__':
main()
в сценарииsave_website_titleФункции делают несколько вещей. Сначала он получает содержимое веб-страницы через сеть, затем использует обычное сопоставление для получения заголовка и, наконец, записывает заголовок в локальный файл. И вот два шага, которые легко сделать неправильно:сетевой запросиоперации с локальными файлами. Итак, в коде мы используем большойtry ... exceptБлок операторов оборачивает эти шаги.Безопасность прежде всего⛑.
Итак, в чем проблема с этим, казалось бы, простым и понятным кодом?
Если у вас есть компьютер с установленным Python рядом с вами, вы можете попробовать запустить скрипт выше. Вы обнаружите, что приведенный выше код не может быть успешно выполнен. И вы также обнаружите, что независимо от того, как вы изменяете значение URL-адреса и целевого файла, программа все равно будет сообщать об ошибке."сохранение не удалось: невозможно...". Зачем?
Проблема заключается в этом огромномtry ... exceptв блоке операторов. Если вы держите глаза близко к экрану, очень внимательно проверьте этот код. Вы заметите, что при написании функции я сделалкрошечная ошибка, я опечатался в методе получения обычной совпадающей строки какobj.grop(1), отсутствует буква "у" (obj.group(1)).
Но именно из-за этого слишком большого и неоднозначного перехвата исключения это должно было быть выброшено из-за неправильного имени метода.AttibuteErrorно был съеден. Это добавляет ненужные проблемы в наш процесс отладки.
Цель перехвата исключений состоит не в том, чтобы поймать как можно больше исключений. Если мы настаиваем с самого начала:Делайте только самый точный захват исключений. Тогда такой проблемы не будет вообще, а точный захват включает в себя:
- Всегда перехватывайте только те блоки операторов, которые могут вызвать исключение
- Старайтесь перехватывать только точные типы исключений, а не расплывчатые.
Exception
Следуя этому принципу, наш пример следует изменить на такой:
from requests.exceptions import RequestException
def save_website_title(url, filename):
try:
resp = requests.get(url)
except RequestException as e:
print(f'save failed: unable to get page content: {e}')
return False
# 这段正则操作本身就是不应该抛出异常的,所以我们没必要使用 try 语句块
# 假如 group 被误打成了 grop 也没关系,程序马上就会通过 AttributeError 来
# 告诉我们。
obj = re.search(r'<title>(.*)</title>', resp.text)
if not obj:
print('save failed: title tag not found in page content')
return False
title = obj.group(1)
try:
with open(filename, 'w') as fp:
fp.write(title)
except IOError as e:
print(f'save failed: unable to write to file {filename}: {e}')
return False
else:
return True
2. Не позволяйте исключениям нарушать абстрактную согласованность
Около четырех или пяти лет назад я работал над проектом внутреннего API для мобильного приложения. Если у вас также есть опыт разработки серверных API, вы должны знать, что для таких систем необходимо разработать набор «спецификаций кодов ошибок API», чтобы клиентам было удобно обрабатывать ошибки вызовов.
Возвращаемый код ошибки выглядит следующим образом:
// HTTP Status Code: 400
// Content-Type: application/json
{
"code": "UNABLE_TO_UPVOTE_YOUR_OWN_REPLY",
"detail": "你不能推荐自己的回复"
}
После формулировки спецификации кода ошибки следующая задача — как ее реализовать. В то время проект использовал фреймворк Django, а страницы ошибок Django были реализованы с использованием механизма исключений. Например, если вы хотите, чтобы запрос возвращал код состояния 404, просто выполнитеraise Http404Вот и все.
Итак, мы, естественно, черпали вдохновение у Джанго. Во-первых, мы определяем класс исключения кода ошибки в проекте:APIErrorCode. Затем в соответствии со «Спецификацией кодов ошибок» записывается множество кодов ошибок, которые наследуют этот класс. Когда вам нужно вернуть сообщение об ошибке пользователю, вам нужно сделать это только один разraiseсделает это.
raise error_codes.UNABLE_TO_UPVOTE
raise error_codes.USER_HAS_BEEN_BANNED
... ...
Неудивительно, что всем нравится возвращать коды ошибок таким образом. Потому что это очень удобно использовать, независимо от того, насколько глубок стек вызовов, до тех пор, пока вы хотите вернуть пользователю код ошибки, вызовитеraise error_codes.ANY_THINGПросто хорошо.
Со временем проекты также разрастались, бросаяAPIErrorCodeвсе больше и больше мест. Однажды, когда я собирался повторно использовать низкоуровневую функцию обработки изображений, я неожиданно столкнулся с проблемой.
Я наткнулся на фрагмент кода, который меня действительно сбивает с толку:
# 在某个处理图像的模块内部
# <PROJECT_ROOT>/util/image/processor.py
def process_image(...):
try:
image = Image.open(fp)
except Exception:
# 说明(非项目原注释):该异常将会被 Django 的中间件捕获,往前端返回
# "上传的图片格式有误" 信息
raise error_codes.INVALID_IMAGE_UPLOADED
... ...
process_imageФункция попытается разобрать файловый объект, бросив, если объект не может быть нормально открыт как изображениеerror_codes.INVALID_IMAGE_UPLOADED (APIErrorCode 子类)исключение, возвращающее вызывающей стороне код ошибки JSON.
Позвольте мне провести вас через этот код с нуля. первоначально написаноprocess_image, хотя я его поставилutil.imageмодуль, но единственное место для вызова этой функции в то время это«Обработка запросов POST для загружаемых пользователем изображений»Вот и все. Чтобы быть ленивым, я позволяю функции бросать напрямуюAPIErrorCodeисключение для завершения работы по обработке ошибок.
Вернемся к проблеме того времени. В то время мне нужно было написать сценарий пакетного изображения, который работает в фоновом режиме, и его можно использовать повторно.process_imageФункция, реализуемая функцией. Но тогда что-то не так, если я хочу переиспользовать функцию, то:
- Я должен поймать файл с именем
INVALID_IMAGE_UPLOADEDисключение- Даже если мои изображения вовсе не из пользовательских загрузок
- я должен импортировать
APIErrorCodeКласс исключений как зависимость для перехвата исключений- Хотя мой скрипт вообще не имеет ничего общего с Django API
** Это результат несовместимости уровней абстракции классов исключений. Значение класса исключения **APIErrorCode состоит в том, чтобы выразить «код ошибки», который может быть непосредственно распознан и использован конечными пользователями (людьми). ** Это одна из абстракций самого высокого уровня во всем проекте. ** Но для удобства мы импортируем и добавляем его в базовый модуль. это ломаетimage.processorАбстрактная согласованность модуля влияет на возможность его повторного использования и ремонтопригодность.
Такая ситуация относится к "модуль бросилвыше чемисключения на уровне абстракции». Чтобы избежать ошибок этого типа, необходимо соблюдать следующие пункты:
- Заставить модули генерировать только исключения, соответствующие текущему уровню абстракции.
- Например
image.processerМодули должны выбрасывать свои собственные оберткиImageOpenErrorаномальный
- Например
- Обёртка исключений и преобразование там, где это необходимо
- Например, модуль обработки изображений
ImageOpenErrorОболочка исключения низкого уровня преобразуется вAPIErrorCodeРасширенное исключение
- Например, модуль обработки изображений
Модифицированный код:
# <PROJECT_ROOT>/util/image/processor.py
class ImageOpenError(Exception):
pass
def process_image(...):
try:
image = Image.open(fp)
except Exception as e:
raise ImageOpenError(exc=e)
... ...
# <PROJECT_ROOT>/app/views.py
def foo_view_function(request):
try:
process_image(fp)
except ImageOpenError:
raise error_codes.INVALID_IMAGE_UPLOADED
за исключением того, что следует избегать метаниявыше чемЗа исключением исключений на текущем уровне абстракции, мы также должны избегать утечек.ниже чемИсключение на текущем уровне абстракции.
если вы использовалиrequestsмодуль, вы, возможно, обнаружили, что он выдает исключение при сбое страницы, а не то, что он использует под капотомurllib3оригинальное исключение модуля, но черезrequests.exceptionsИсключение, которое запаковывается один раз.
>>> try:
... requests.get('https://www.invalid-host-foo.com')
... except Exception as e:
... print(type(e))
...
<class 'requests.exceptions.ConnectionError'>
Это также сделано для обеспечения абстрактной согласованности классов исключений. Поскольку модуль urllib3 — это базовая деталь реализации, от которой зависит модуль запросов, и эта деталь может измениться в будущих версиях. Следовательно, исключения, которые он выдает, должны быть правильно упакованы, чтобы избежать будущих базовых изменений.requestsЛогика обработки ошибок на стороне пользователя оказывает влияние.
3. Обработка исключений не должна брать верх
Ранее мы упоминали, что захват исключений должен быть точным, а уровень абстракции должен быть последовательным. Но в реальном мире, если вы будете строго следовать этим процессам, вы, скорее всего, столкнетесь с другой проблемой:Слишком много логики обработки исключений, которая нарушает основную логику кода.. Специфическая производительность заключается в том, что код наполнен большим количествомtry,except,raiseУтверждения, которые затрудняют определение основной логики.
Давайте посмотрим пример:
def upload_avatar(request):
"""用户上传新头像"""
try:
avatar_file = request.FILES['avatar']
except KeyError:
raise error_codes.AVATAR_FILE_NOT_PROVIDED
try:
resized_avatar_file = resize_avatar(avatar_file)
except FileTooLargeError as e:
raise error_codes.AVATAR_FILE_TOO_LARGE
except ResizeAvatarError as e:
raise error_codes.AVATAR_FILE_INVALID
try:
request.user.avatar = resized_avatar_file
request.user.save()
except Exception:
raise error_codes.INTERNAL_SERVER_ERROR
return HttpResponse({})
Это функция просмотра, которая обрабатывает загрузку аватаров пользователями. В этой функции выполняются три вещи, и для каждой из них перехватываются исключения. Если во время выполнения чего-либо возникает исключение, верните во внешний интерфейс удобную для пользователя ошибку.
Хотя этот поток обработки разумен, очевидно, что логика обработки исключений в коде немного «непосильна». На первый взгляд, это все отступы кода, и сложно извлечь основную логику кода.
Еще в версии 2.5 язык Python уже предоставлял инструмент для таких сценариев: «менеджер контекста». Контекстный менеджер — это галстукwithСпециальный объект Python, используемый оператором для упрощения обработки исключений.
Итак, как мы можем использовать контекстные менеджеры для улучшения нашего процесса обработки исключений? Перейдем непосредственно к коду.
class raise_api_error:
"""captures specified exception and raise ApiErrorCode instead
:raises: AttributeError if code_name is not valid
"""
def __init__(self, captures, code_name):
self.captures = captures
self.code = getattr(error_codes, code_name)
def __enter__(self):
# 刚方法将在进入上下文时调用
return self
def __exit__(self, exc_type, exc_val, exc_tb):
# 该方法将在退出上下文时调用
# exc_type, exc_val, exc_tb 分别表示该上下文内抛出的
# 异常类型、异常值、错误栈
if exc_type is None:
return False
if exc_type == self.captures:
raise self.code from exc_val
return False
В приведенном выше коде мы определяемraise_api_errorдиспетчер контекста, который ничего не делает при входе в контекст. Однако при выходе из контекста он будет определять, выбрасывается ли тип в текущем контексте.self.capturesисключение, если есть, используйтеAPIErrorCodeВ качестве альтернативы это класс исключений.
После использования этого контекстного менеджера всю функцию можно сделать более понятной и лаконичной:
def upload_avatar(request):
"""用户上传新头像"""
with raise_api_error(KeyError, 'AVATAR_FILE_NOT_PROVIDED'):
avatar_file = request.FILES['avatar']
with raise_api_error(ResizeAvatarError, 'AVATAR_FILE_INVALID'),\
raise_api_error(FileTooLargeError, 'AVATAR_FILE_TOO_LARGE'):
resized_avatar_file = resize_avatar(avatar_file)
with raise_api_error(Exception, 'INTERNAL_SERVER_ERROR'):
request.user.avatar = resized_avatar_file
request.user.save()
return HttpResponse({})
Подсказка: рекомендуется к прочтениюPEP 343 -- The "with" Statement | Python.org, чтобы узнать больше о менеджерах контекста.
модульcontextlibСуществует также множество служебных функций и примеров, связанных с написанием контекстных менеджеров.
Суммировать
В этом посте я делюсь тремя советами, связанными с обработкой исключений. Наконец, резюмируя основные моменты:
- Только операторы catch, которые могут генерировать исключения, чтобы избежать неоднозначной логики catch.
- Сохраняйте абстрактную согласованность класса исключений модуля и при необходимости обертывайте базовый класс исключений.
- Используйте диспетчеры контекста для упрощения повторяющейся логики обработки исключений.
После прочтения статьи у вас есть на что пожаловаться? Пожалуйста, оставьте сообщение или наПроблемы проекта GitHubскажите мне.
приложение
- Изображение предоставлено: фото Бернарда Хермана на Unsplash.
- Адреса других серий статей: https://github.com/piglei/one-python-craftsman
Другие статьи цикла: