Архив метки: file system

Исключения в Питоне

Поговорим об исключениях.

Всё нижеизложенное относится к Python 3.3, хотя отчасти справедливо и для более ранних версий.

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

Рассмотрим простейший пример: открытие файла. Если всё нормально — open(filename, 'r') возвращает объект этого самого файла, с которым можно делать всякие полезные вещи: читать из него данные и т.д.

Если файл не может быть открыт — выбрасывается исключение:

try:
f = open(filename, 'r')
try:
print(f.read())
finally:
f.close()
except OSError as ex:
print("Cannot process file", filename, ": Error is", ex)

Открываем файл и печатаем его содержимое.

Обратите внимание: файл нужно не только открыть но и закрыть после использования. Исключение может выбросить open (например, если файла нет на диске или нет прав на его чтение).

Если файл открыт — читаем его через f.read(). Этот вызов тоже может выбросить исключение, но файл закрывать всё равно нужно. Поэтому необходим блок finally: f.close() должен быть вызван даже если f.read() сломался. В этом месте удобней было бы воспользоваться конструкцией with но мы же сейчас говорим об исключениях а не о контекстных менеджерах, верно?

Исключения из обоих мест попадут в except OSError, где можно будет что-то сделать с ошибкой.

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

Введение закончено. Теперь сконцентрируемся на том что происходит в except.

Типы исключений

Все исключения составляют иерархию с простым наследованием. Вот простой небольшой кусочек от довольно обширного набора исключений ядра Питона:

BaseException
+-- SystemExit
+-- KeyboardInterrupt
+-- GeneratorExit
+-- Exception
+-- StopIteration
+-- AssertionError
+-- AttributeError
+-- BufferError

Самый базовый класс — BaseException. Он и его простые потомки (SystemExit, KeyboardInterrupt, GeneratorExit) не предназначены для перехвата обыкновенным программистом — только Питон и редкие библиотеки должны работать с этими типами. Нарушение правила ведет, например, к тому что программу невозможно корректно завершить — что совсем не хорошо.

Также не нужно перехватывать все исключения:

try:
...
except:
...

работает как

try:
...
except BaseException:
...

Всё, что может быть нужно программисту — это Exception и унаследованные от него классы.

Вообще-то лучше ловить как можно более конкретные классы исключений. Например, в случае с файлом это OSError или даже может быть FileNotFoundError. Таким образом мы не перехватим AttributeErrorили ValueError, которые в этом примере означали бы ошибку или опечатку программиста.

Кстати, обратите внимание: StopIteration порожден от Exception а GeneratorExit от BaseException. Подробности, почему сделано именно так, можно найти в PEP 342.

Цепочки исключений

Прочитав предыдущую главку все прониклись необходимостью указывать правильный класс исключений и пообещали никогда не использовать BaseException.

Идем дальше. Следующий пример:

try:
user = get_user_from_db(login)
except DBError as ex:
raise UserNotFoundError(login) from ex

Получаем пользователя из базы данных чтобы что-то потом с ним сделать. get_user_from_db может выбросить ошибку базы данных. Для нас это скорее всего означает что такого пользователя нет. Но для логики приложения полезней наш собственный тип UserNotFoundError с указанием логина проблемного пользователя, а не обезличенная ошибка БД — что мы и выбрасываем в обработчике исключения.

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

Для таких целей служит конструкция raise ... from ....

По PEP 3134 у объекта исключения имеется несколько обязательных атрибутов.

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

Затем — __context__. Если исключение было создано в ходе обработки другого исключения (выброшено из except блока) — __context__будет содержать то самое породившее исключение. Которое, в свою очередь тоже может иметь установленный __context__. Этот атрибут равен None если наше исключение — самое первое и н
е имеет предшественников.

__context__ устанавливается автоматически.

В отличие от контекста __cause__ устанавливается только если исключение было выброшено конструкцией raise ... from ... и равно значению from.

Если исключение выбрасывалось простым raise ... то __cause__ будет равно None в то время как __context__ всегда будет содержать породившее исключение если оно существует.

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

И тут тоже есть проблема: печатается либо явная цепочка если есть установленный __cause__ или неявная, тогда используется __context__.

Иногда программисту может быть нужно отбросить породившие исключения как не имеющие смысла при выводе traceback. Для этого появилась форма записи

raise exc from None

PEP 409 и PEP 415 рассказывают как это работает:

У исключения всегда есть атрибут __supress_context__. По умолчанию он равен False.

Конструкция raise ... from ... записывает fromв __cause__ и устанавливает __supress_context__ в True.

Тогда семейство функций traceback.print_exc() печатают цепочку если явно указан (не равен None) __cause__ или есть __context__ и при этом __supress_context__ равен False.

Изложение получилось несколько длинным, но сократить текст без потери смысла у меня не вышло.

Семейство OSError

Последняя проблема о которой хотелось бы рассказать — это типы исключений порожденные вызовами операционной системы.

До Python 3.3 существовало много разных типов таких исключений: os.error, socket.error, IOError, WindowsError, select.errorи т.д.

Это приводило к тому, что приходилось указывать несколько типов обрабатываемых исключений одновременно:

try:
do_something()
except (os.error, IOError) as ex:
pass

Ситуация на самом деле была еще хуже: очень легко забыть указать еще одно нужное исключение, которое может внезапно прилететь. Дело в том что исключения операционной системы часто никак не проявляют себя при разработке. На машине программиста всё работает отлично и он не подозревает о возможных проблемах. Как только программа выходит в production пользователь вдруг ловит что-то неожиданное и программа аварийно завершается. Все опечалены.

Проблема решена в PEP 3151: весь этот зоопарк теперь является псевдонимами для OSError. Т.е. пишите OSError и не ошибетесь (прочие имена оставлены для обратной совместимости и облегчения портирования кода на новую версию).

Давайте рассмотрим ещё один аспект исключений, порожденных операционной системой.

У OSError есть атрибут errno, который содержит код ошибки (список всех возможных символьных констант для ошибок можно посмотреть в модуле errno).

Открываем файл, получаем OSError в ответ. Раньше мы должны были анализировать ex.errno чтобы понять, отчего произошла ошибка: может файла нет на диске, а может нет прав на запись — это разные коды ошибок (ENOENT если файла нет и EACCES или EPERM если нет прав).

Приходилось строить конструкцию вроде следующей:

try:
f = open(filename)
except OSError as ex:
if ex.errno == errno.ENOENT:
handle_file_not_found(filename)
elif ex.errno in (errno.EACCES, errno.EPERM):
handle_no_perm(filename)
else:
raise # обязательно выбрасывать не обработанные коды ошибки

Теперь иерархия расширилась. Привожу полный список наследников OSError:

OSError
+-- BlockingIOError
+-- ChildProcessError
+-- ConnectionError
| +-- BrokenPipeError
| +-- ConnectionAbortedError
| +-- ConnectionRefusedError
| +-- ConnectionResetError
+-- FileExistsError
+-- FileNotFoundError
+-- InterruptedError
+-- IsADirectoryError
+-- NotADirectoryError
+-- PermissionError
+-- ProcessLookupError
+-- TimeoutError

Наш пример можем переписать как:

try:
f = open(filename)
except FileNotFound as ex:
handle_file_not_found(filename)
except PermissionError as ex:
handle_no_perm(filename)

Гораздо проще и понятней, правда? И меньше мест, где программист может ошибиться.

Заключение

Переходите на Python 3.3, если можете. Он хороший и облегчает жизнь.

Новые плюшки в вопросе, касающемся исключений, я показал.

Если использовать новый питон не позволяют обстоятельства — пишите на чём есть, но помните как правильно это делать.

Автор: Andrew Svetlov

Python 3: Импорт и юникод

Третий питон с рождения замечательно поддерживает юникод. Собственно говоря, это одна из самых заметных его особенностей.

Русские идентификаторы

Чуть меньше бросается в глаза тот факт, что идентификаторы тоже стали юникодными. Уважаемые читатели, если вы используете третий питон и недостаточно хорошо владеете английским — пишите по русски. Это выглядит гораздо лучше, чем убогое средство под названием «транслитерация». Оцените сами:

def функция(агрумент):
коэффициент = 5
return агрумент * коэффициент

Это на самом деле здорово!

Еще один не вполне очевидный момент: имена модулей тоже могут быть в юникоде:

from . import вспомогательный_модуль

Тоже выглядит неплохо, верно? Есть только одна небольшая проблема: это не всегда работает. Вернее, на Windows возможны неприятности. И не нужно заявлять, что вопросы, касающиеся самой популярной на сегодняшний день операционной системы — никого не волнуют. Подавляющее большинство разработчиков самого Питона Windows не используют — и тем не менее Питон обязан на ней работать, и работать хорошо.

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

Юникод в C API

В Python 2 немалая часть Python C API принимала char * там, где требовалась строка. Поскольку str и был последовательностью байт — сложностей не возникало.

При переносе кода на Python 3 нужно было с этим что-то делать: strстал юникодным типом, последовательностью символов.

Но в С нет удобного типа для unicode! Вернее, существует стандартный тип wchar_t, который обременен множеством проблем. Главные из них: в разных реализациях этот тип имеет различный размер: 16 бит для UCS-2 и 32 бита для UCS-4. К тому же Windows (о, снова она) не поддерживает UCS-2 в полной мере (UCS-4 не поддерживает совсем).

Хуже всего то, что на некоторых платформах этот wchar_t попросту не определен.

Таким образом, использовать wchar_t в Python C API нельзя.

Сам Питон вводит тип Py_UNICODE для этих целей. Но и тут не все гладко. Этот тип не входит в Limited API (PEP 384).

Кроме того, разработчики не хотели радикально заменить все char * на что-то другое.

Есть еще и вопрос практического удобства: ведь очень здорово писать

ret = PyObject_GetAttrString(obj, "attribute");

Для wchar_t все гораздо сложнее, далеко не все компиляторы поддерживают строковые юникодные константы.

В свете вышеописанных причин Python C API продолжает использовать char *, считая, что эти строки имеют кодировку UTF-8 если явно не указано иное. Т.е. прототипы функций C API выглядят как:

PyObject *
PyImport_ImportModuleLevel(char *name, PyObject *globals,
PyObject *locals, PyObject *fromlist,
int level);

Это — импорт модуля с именем name, которое передается как UTF-8строка, аналог питоновской функции __import__.

И эта функция — лишь верхушка используемого механизма. В процессе импорта вызываются довольно много внутренних закрытых функций — и везде используются переменные вроде char *name в качестве имен модулей. В кодировке UTF-8, еще раз напомню.

А ведь имя модуля транслируется в путь к файлу! А кодировака файловой системы может отличаться от UTF-8. Счастливые пользователи Linux давно об этом забыли — в подавляющем большинстве систем по умолчанию как кодировка пользователя (переменная окружения LANG) так и файловой системы установлены в UTF-8 и проблем нет совсем. Но в общем случае это не всегда так.

Кодировки по умолчанию

Чуть-чуть о кодировках. Для определения используемых по умолчанию кодировок в питоне существуют три функции: sys.getdefaultencoding, sys.getfilesystemencoding и locale.getpreferredencoding.

  • sys.getdefaultencoding() — кодировка по умолчанию, используемая в питоновских исходниках. Для третьего питона всегда равна UTF-8. Это — та самая кодировка, которую можно перекрыть написав в начале файла

    # -*- encoding: utf-8 -*-
  • sys.getfilesystemencoding() — кодировка файловой системы. Например, для

    f = open('path/to/file', 'r')

    значение 'path/to/file' имеет тип str (юникод). Лежащая в основе функция из clib имеет прототип

    int open(const char *pathname, int flags, mode_t mode);

    Значит, 'path/to/file' должен быть преобразован в char *используя к
    одировку sys.getfilesystemencoding(). Конечно, в Python C API есть специальные функции для этого.

  • locale.getpreferredencoding() — предпочтительная для пользователя кодировка. Она устанавливается в региональных настройках и к файловой системе прямого отношения не имеет.

Теперь снова вспомним нашу горячо любимую Windows.

locale.getpreferredencoding() возвращает 'cp1251' — Windows настроена на русский язык. Кодировка для консоли (sys.stdout.encoding) другая, это 'cp866' — что добавляет сумбура в и без того запутанную проблему. Ну да ладно, не будем отвлекаться.

sys.getfilesystemencoding() возвращает 'mbcs'. И вот здесь начинаются основные чудеса. Обратите внимание, mbcs — это не cp1251. Равно как и не cp1252 или какая другая кодировка. mbcs — это нечто совершенно особенное!

Multibyte character set (кодировка MBCS)

При преобразовании mbcs -> unicode используется кодировка из locale.getpreferredencoding(), преобразование однозначное и проблем не вызывает.

Для обратного преобразования unicode -> mbcs тоже используется locale.getpreferredencoding() (cp1251 в нашем случае). Но cp1251 не может описать любой юникодный символ. А mbcs — хитрый и коварный. Если для символа не существует точного преобразования — используется ближайший похожий по начертанию.

Это непросто понять без примера. Давайте возьмем французское слово comédie и попробуем преобразовать его в mbcs, имея руский язык cp1251 в настройках по умолчанию.

Возьмем Python 3.1:

>>> b = b'comxc3xa9die'
>>> s = b.decode('utf8')
>>> s.encode('mbcs')
b'comedie'

Посмотрите, какая прелесть! Для символа é в русской раскладке cp1251 нет подходящего аналога. Но ведь английская буква e так похожа: нужно лишь убрать умляут (англ. umlaut, французы зовут этот знак accent aigu). Так и получили преобразование comédie -> comedie без единой ошибки.

А теперь представьте, что это — имя файла. Результат будет следующим: файл на диске есть, и так как в Windows файловая система юникодная, имя файла будет записано правильно, по французски. Но преобразование unicode -> mbcs даст несколько другое имя, которого на диске нет.

В результате получается изумительная по своей красоте ситуация:

f = open('comédie', 'r')

будет говорить, что файла нет — а на самом деле вот же он, красавец!

Справедливости ради нужно упомянуть, что в Python 3.2 поведение mbcsнемного поменялось, и 'comédie'.encode('mbcs') вызовет UnicodeEncodeError. Дело в том, что mbcs стал использовать режим strict по умолчанию. Чтобы повторить функциональность 3.1 следует указывать режим replace: 'comédie'.encode('mbcs', 'replace')

Юникодная файловая система

С mbcs мы разобрались и выяснили, что для работы с файловой системой эта кодировка в общем случае непригодна. Т.е. если я буду использовать русские имена файлов на русской Windows — всё будет хорошо. Но открыть этот файл у американца или голландца не выйдет. Что же делать?

В Windows помимо open есть еще и функция

FILE *_wfopen(const wchar_t *filename, const wchar_t *mode);

которая принимает wchar_t * и позволяет использовать оригинальное юникодное имя файла без всяких преобразований. Существует целый набор, начинающийся с _w — на все случаи жизни.

Значит, нужно делать следующее: для Windows использовать юникодные версии функций работы с файлами, а для всех остальных операционных систем применять .encode(sys.getfilesystemencoding()).

Реализация модуля io начиная с версии 3.1 так и поступает.

И снова импорт русских названий

Всё отлично за одним маленьким исключением — механизм импорта не использует io! Исторически сложилось так, что имя импортируемого модуля довольно быстро преобразовывается в sys.getfilesystemencoding() (с возможными ошибками и потерями, о которых я писал выше) и в таком виде пронизывает весь очень непростой и громоздкий код, чтобы попасть в функции стандартной библиотеки C.

Добавьте к этому довольно большой объем платформозависимого кода (на Маке все работает совсем не так, как на Linux) и проблему обратной совместимости (даже после объявления части API устаревшей она должна поддерживаться как минимум в двух следующих выпусках) — и вы сможете представить сложность и объемность задачи.

Так вот, после трехлетнего труда (с небольшими перерывами, естественно — это же добровольный некоммерческий Open Source) Victor Stinner завершил требуемое нелегкое преобразование. Дов
ольно незаметный, но очень важный шаг!

Файловые пути стали храниться в PyObject* (на самом деле это, конечно, strPyUnicodeObject), работающая с ними часть C APIимеет суффикс Object. Например:

PyObject *
PyImport_ImportModuleLevelObject(PyObject *name, PyObject *globals,
PyObject *locals, PyObject *fromlist,
int level);

Сравните с PyImport_ImportModuleLevel. Все функции из старого APIстали тонкими обертками над новыми вариантами. Так, PyImport_ImportModuleLevel создает PyObject из name и вызывает PyImport_ImportModuleLevelObject.

Эти старые функции оставлены для сохранения обратной совместимости, сам Питон их уже не использует.

Если быть честным, именно Windows поддержка чуть-чуть не готова — но до выхода Python 3.3 еще очень много времени. Достаточно, чтобы закончить работу и навести полный порядок.

Заключение

Я написал этот довольно длинный текст преследуя несколько целей:

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

  • Вторая — продемонстрировать, как работают кодировки применительно к файловой системе.

  • Третья — напомнить, что можно использовать русские буквы в идентификаторах. Комментарии излишни.

  • И, наконец, очень хотелось отметить завершение отлично выполненной работы, которая делает Питон немного лучше.

Автор: Andrew Svetlov