Вступление:
Стандартная иерархия исключений — важная часть языка Python. При этом она имеет два определяющих свойства: она с одной стороны общая, а с другой - селективная. Общая, в данном случае, значит, что один и то же тип исключения может быть возбуждён и перехвачен вне зависимости от контекста (например, когда вы пытаетесь прибавить что-то к числу, вызвать строковый метод или записать объект в сокет, при неверных аргументах будет вызвано исключение TypeError). Селективное означает, что мы можем какие-то исключения перехватывать, обрабатывать, сохранять, инкапсулировать, а какие-то выпускать на более высокий уровень перехвата. Например, вы можете перехватывать ZeroDivisionErrors и пропускать другие типы ArthmeticErrors (например, OverflowError).
Данный PEP предлагает внести изменения в часть этой иерархии для лучшего воплощения данных принципов; он касается ошибок, связанных с системными вызовами (OSError, IOError, mmap.error, select.error и все их подклассы).
Предложение:
Сбивающий с толку набор исключений, связанных от ОС
Исключения, вызванные ОС или системными вызовами, на данный момент представляют из себя различные классы, организованные в следующую иерархию:
На самом деле, тяжело представить ситуацию, где надо было бы перехватить OSError и пропустить IOError, или наоборот.
Ещё один минус такой двусмысленной сегментации состоит в том, что сама стандартная библиотека не всегда может чётко понять, какое исключение ей надо возбудить. Например, в модуле theselect одинаковые ошибки вызывают select.error, OSError или IOError в зависимости от того, используете ли вы select(), poll, kqueue или epoll объекты. Это делает пользовательский код бессмысленно сложным, так он должен быть готов перехватывать различные типы исключений, в зависимости от того, какая реализация будет выбрана в момент использования кода.
Что же касается WindowsError, так этот тип вообще выглядит бесполезным. Во-первых, этот тип исключений относится исключительно к Windows, что добавляет необоснованной сложности при создании кросс-платформенного кода (такой код можно найти в Lib/shutil.py). Во-вторых, он является дочерним классов OSError и возбуждается при тех же ошибках, что и OSError на других системах. В-третьих, пользователи, желающие доступа к низкоуровневым исключениям по любому должны будут проверять errno или winerrorattribute.
Заметка:
В приложении Б приведен обзор различных типов исключений интерпретатора и стандартной библиотеки.
Отсутствие детально структурированных исключений
Текущее разнообразие связанных с ОС исключений не позволяет пользователю легко отфильтровать желаемые типы ошибок. Как пример, представим, что нам надо удалить файл, если он существует. Подход Look Before You Leap (Посмотри Перед Тем, Как Прыгать, LBYL) предполагает очевидный путь решения этой проблемы:
По этой причине более правильным подходом будет попробовать удалить файл и проигнорировать ошибку, если окажется, что он не существует. Этот подход известен как Easier to Ask Forgiveness than to get Permission (Проще Попросить Прощения, чем Спросить Разрешения, EAFP). Правильный код будет выглядеть так (и будет работать и на POSIX и на Windows):
Реструктуризация иерархии исключений очевидно изменит семантическое значение части существующего кода. Поскольку улучшение существующей ситуации невозможно без изменения этой семантики, необходимо определить узкий тип совместимости, который мы назовем "полезной совместимостью".
Для этого, во-первых, мы должны объяснить, что мы имеем ввиду под осторожным или небрежным перехватом исключений. Небрежный, он же "наивный" код — это код, который вслепую перехватывает любое исключение типа OSError, IOError, socket.error, mmap.error, WindowsError, select.error без проверки атрибута errno. Причина такого названия в том, что эти типы исключений слишком широки, чтобы обозначать что-либо. Любое из них может быть возбуждено при различных условиях, например: плохой дескриптор файла (что обычно обозначает программную ошибку), сокет без соединения (то же самое), таймаут сокета, несоответствие типа файла, неверный аргумент, ошибка передачи, недостаточные права, несуществующая директория, переполнение файловой системы и т.д.
(более того, использование некоторых этих исключений не постоянно, смотрите Приложение А, где приведен обзор модуля select, который возбуждает различные исключения в зависимости от реализации)
Осторожный код — это код, который при перехвате любого из вышеупомянутых исключений, проверяет его errno атрибут, чтобы выяснить условия возбуждения исключения и обработать его в соответствии с этим.
Теперь мы можем дать определение полезной совместимости:
Осторожный код, с другой стороны, не должен пострадать, так как одна из целей этого PEP — облегчить написание такого кода.
Шаг 1: объединение типов исключений
Первый шаг на нашем пути — соединение существующих типов исключений. Предложены следующий изменения:
Каждое из этих изменений может быть принято или отвергнуто по отдельности, но, очевидно, что наилучший результат будет достигнут после принятия всех предложенных изменений.
В таком случае иерархия IO исключений будет такой:
Этот шаг не только позволит получить более простой ландшафт исключений, как говорилось в Предложении, но и позволит достичь более полного осуществления второго шага (см. пункт Предварительные требования)
Причина сохранения OSError в качестве имени для всех исключений, зависящих от операционной системы, в том, что оно более общее, чем IOError. EnvironmentError гораздо дольше набирать да и менее известен.
Обзор в Приложении Б показывает, что IOError на данный момент является преобладающим исключением в стандартной библиотеке. Для стороннего кода Google Code Search показывает, что IOError в десять раз популярнее, чем EnvironmentError и в три раза чем OSError. И тем не менее, при всём уважении к IOError, меньшая известность OSError не является препятствием для нашего предложения.
Атрибуты исключений
Так как WindowsError включается в OSError, последний тип получает winerror атрибут в Windows. Он устанавливается в None в тех ситуациях, когда он не имеет смысла, как уже происходит с errno, filename и sterror атрибутами (например, когда OSError возбуждается напрямую кодом)
Неодобряемые имена
Следующие параграфы содержат краткий обзор возможных стратегий введения неодобряемых имен исключений.На данный момент все имена продолжают существовать как псевдонимы. Это решение может быть пересмотрено в релизе 4,0
встроенные исключения
Отказ от старых встроенных исключений не может быть проведена топорно, просто перехватывая все их имена во встроенном пространстве имён, так как это критично отразится на производительности. Мы так же не можем работать на объектном уровне, так как неодобряемые имена будут псевдонимами для одобряемых объектов (не спрашивайте меня, что тут имелось ввиду:) )
Решением может быть распознавание этих имен в процессе компиляции и выдача отдельного опкода LOAD_OLD_GLOBAL вместо обычного LOAD_GLOBAL. Этот опкод перехватывает вывод DeprecationWarning (или PendingDeprecationWarning, в зависимости от политики, определённой в результате) в случае, если имя не существует в глобальном пространстве имён, но только если оно является встроенным. Этого достаточно чтобы избежать ложных срабатываний (в случае если кто-то определяет свой собственный OSError в модуле), а ошибки будут достаточно редки (например, если кто-то получает доступ к OSError через thebuiltins модуль, а не напрямую)
исключения, определяемые в модулях
Изложенный выше подход не может быть просто использован в данном случае, так как он требует специальной обертки для модулей во время их компиляции. Однако, эти имена менее видимы (они не попадают во встроенное пространство имён) и так же менее известны, так что мы можем сохранить им жизнь в их собственных пространствах имён.
Шаг 2: определение дополнительных подклассов
Второй шаг нашего пути — расширение иерархии путём определения подклассов, которые и будут возбуждаться, вместо родительского класса с некоторым errno. Конкретные значения errno — предмет для обсуждения, но обзор существующих исключений (см Приложение А) поможет нам предложить разумный набор всех значений. Альтернативный вариант выдачи каждому errno мнемонического имени выглядит глупым, бессмысленным и лишь загрязнит корневое пространство имён.
Более того, в некоторых случаях, различные errno соответствуют одному и тому же подклассу. Например, EAGAIN, EALREADY, EWOULDBLOCK и EINPROGRESS используются для того, чтобы сообщить, что операция над не блокируемым сокетом будет заблокирована (и потому надо попробовать ещё раз позже). Поэтому все они могут возбуждать один и тот же подкласс и оставить на усмотрение пользователя проверку errno (смотрите ниже "атрибуты исключений").
Предварительные требования
Шаг 1 — необязательное предварительное требования для этого шага.
Предварительное требование, так как некоторые errno могут относиться к различным классам исключений: например, ENOENT может быть присоединён как к OSError так и к IOError, в зависимости от контекста. Если мы не хотим нарушать полезную совместимость, мы не можем допустить, чтобы OSError или IOError не подпадал под соответствие выражению except там, где сейчас оно срабатывает.
Необязательное, так как мы можем выполнить второй шаг и без объединения соответствующих классов. Например, ENOENT может возбуждать гипотетическое исключение FileNotFoundError в случае, если перед этим было возбуждено IOError, или OSError во всех остальных случаях.
Зависимость от первого шага может быть в принципе полностью устранена, если новые подклассы будут иметь множественное наследование, чтобы соответствовать всем существующим подклассам (или, как минимум, OSError и IOError, как наиболее распространённым). Но такой вариант сделает иерархию более сложной и потому тяжелее воспринимаемым пользователями.
Новые классы исключений
Следующий предварительный список подклассов вместе с их описанием и соответствующими errno является объектом для обсуждения:
По поводу имён могут возникать различные споры. В частности, должны ли все имена исключений заканчиваться на Error. За — согласованность с остальной иерархией исключений, против — краткость (особенно для длинных имён, таких как ConnectionAbortedError).
Атрибуты исключений
Для того, чтобы сохранить полезную совместимость, данные подклассы должны устанавливать адекватные значения различных атрибутов исключений, в зависимости от суперкласса (например, errno, filename и, возможно, winerror).
Реализация
Так как предполагается, что подклассы будут возбуждаться на основе значения errno, то изменения в модулях расширения (стандартных или сторонних) если и потребуются, то будут минимальны.
Первая возможность — адаптировать семейство функций PyErr_SetFromErrno() (PyErr_SetFromWindowsErrno() под Windows) для возбуждения соответствующего подкласса OSError. Однако, это не покроет код на Python, который возбуждает OSError напрямую, используя такой код (из Lib/tempfile.py):
Возможные возражения
Загрязнение пространства имён
Более тонкая иерархия делает корневое (или встроенное) пространство имён более заполненным. Однако, разрастание пространства имён будет ограниченным, так как:
Ранние обсуждения
Хотя это и первое формальное предложение, тем не менее данная идея уже получала поддержку в прошлом и касательно детализации иерархии исключений и касательно слияния OSError и IOError.
Удаление WindowsError уже обсуждалось, но было отклонено в рамках другого PEP, однако и там пришли к консенсусу, что выделение этого типа из OSError не было осмысленным, что говорит в пользу слияния с OSError.
Реализации
Эталонная реализация была включена в Python 3.3. Разработка проводилась на http://hg.python.org/features/pep-3151/ в ветке pep-3151, багтрекер находился тут. Реализация была успешно протестирована на различных системах: Linux, Windows, OpenIndiana и FreeBSD.
Одним из источников проблем были соответствующие конструкторы OSError и WindowsError, которые оказались несовместимыми. Решили это сохранением сигнатуры OSError и добавлением четвёртого опционного аргумента для передачи Windows error code (отличающегося от POSIX errno). В четвёртом аргументе сохранен winerror, соответствующий POSIX errno. PyErr_SetFromWindowsErr* функции были адаптированны для правильного вызова конструктора.
Есть одна маленькая сложность когда функции PyErr_SetFromWindowsErr* вызываются из OSError а не из WindowsError: в атрибуте errno объекта исключения сохранится Windows error code (например, 109 для ERROR_BROKEN_PIPE) а не его POSIX аналог (32 для EPIPE), как это делается сейчас. Для кодов ошибок не связанных с сокетом это касается только частного модуля _multiprocessing, так что нет причин для беспокойства.
Заметка
Для ошибок в сокете POSIX errno, как показано в модуле errno, численно равно Windows Socket error code, возвращаемому системным вызовом WSAGetLastError:
Соответствие шаблонам
Альтернативная возможность представлена продвинутым соответствием шаблонам при перехвате исключений. Например:
Данный PEP игнорирует EOFError, что сигнализирует о прерванном вводе из потока для различных протоколов и реализаций форматов файлов (например, GzipFile). EOFError не связан с IOError или OSError, это логическая ошибка, возбуждаемая на высоком уровне.
Данный PEP так же игнорирует SSLError, возбуждаемый модулем ssl для распространения ошибок, обозначенных в библиотеке OpenSSL. В идеале, SSLError выиграл бы от подобного подхода, но для него требуется отдельная обработка, так как он определяет свои собственные константы типов ошибок (ssl.SSL_ERROR_WANT_READ и тд.). В Python 3.2 SSLError уже заменён socket.timeout в случае таймаута сокета (см проблему 10272)
И, наконец, судьба socket.gaierror и socket.herror ещё не определена. Хотя они заслуживают менее таинственных имён, это может быть сделано вне зависимости от пересмотра иерархии исключений.
Стандартная иерархия исключений — важная часть языка Python. При этом она имеет два определяющих свойства: она с одной стороны общая, а с другой - селективная. Общая, в данном случае, значит, что один и то же тип исключения может быть возбуждён и перехвачен вне зависимости от контекста (например, когда вы пытаетесь прибавить что-то к числу, вызвать строковый метод или записать объект в сокет, при неверных аргументах будет вызвано исключение TypeError). Селективное означает, что мы можем какие-то исключения перехватывать, обрабатывать, сохранять, инкапсулировать, а какие-то выпускать на более высокий уровень перехвата. Например, вы можете перехватывать ZeroDivisionErrors и пропускать другие типы ArthmeticErrors (например, OverflowError).
Данный PEP предлагает внести изменения в часть этой иерархии для лучшего воплощения данных принципов; он касается ошибок, связанных с системными вызовами (OSError, IOError, mmap.error, select.error и все их подклассы).
Предложение:
Сбивающий с толку набор исключений, связанных от ОС
Исключения, вызванные ОС или системными вызовами, на данный момент представляют из себя различные классы, организованные в следующую иерархию:
- EnvironmentError
- IOError
- io.BlockingIOError
- io.UnsupportedOperation (так же является подклассом ValueError)
- socket.error
- socket.gaierror
- socket.herror
- socket.timeout
- OSError
- VMSError
- WindowsError
- mmap.error
- select.error
>>> os.remove("fff") Traceback (most recent call last): File "<stdin>", line 1, in <module> OSError: [Errno 2] No such file or directory: 'fff' >>> open("fff") Traceback (most recent call last): File "<stdin>", line 1, in <module> IOError: [Errno 2] No such file or directory: 'fff'Одна и та же проблема (отсутствующий файл) вызывает два разных типа исключения, в зависимости от того, функция из какой библиотеки вызывается. В данном случае модуль os возбуждает OSError (или его подкласс WindowsError), тогда как модуль io возбуждает IOError. При всём при этом, пользователя интересует суть ошибки, а не то, в каком модуле она возникла (так как последнее можно и так явно понять из трассировочного сообщения или исходного кода).
На самом деле, тяжело представить ситуацию, где надо было бы перехватить OSError и пропустить IOError, или наоборот.
Ещё один минус такой двусмысленной сегментации состоит в том, что сама стандартная библиотека не всегда может чётко понять, какое исключение ей надо возбудить. Например, в модуле theselect одинаковые ошибки вызывают select.error, OSError или IOError в зависимости от того, используете ли вы select(), poll, kqueue или epoll объекты. Это делает пользовательский код бессмысленно сложным, так он должен быть готов перехватывать различные типы исключений, в зависимости от того, какая реализация будет выбрана в момент использования кода.
Что же касается WindowsError, так этот тип вообще выглядит бесполезным. Во-первых, этот тип исключений относится исключительно к Windows, что добавляет необоснованной сложности при создании кросс-платформенного кода (такой код можно найти в Lib/shutil.py). Во-вторых, он является дочерним классов OSError и возбуждается при тех же ошибках, что и OSError на других системах. В-третьих, пользователи, желающие доступа к низкоуровневым исключениям по любому должны будут проверять errno или winerrorattribute.
Заметка:
В приложении Б приведен обзор различных типов исключений интерпретатора и стандартной библиотеки.
Отсутствие детально структурированных исключений
Текущее разнообразие связанных с ОС исключений не позволяет пользователю легко отфильтровать желаемые типы ошибок. Как пример, представим, что нам надо удалить файл, если он существует. Подход Look Before You Leap (Посмотри Перед Тем, Как Прыгать, LBYL) предполагает очевидный путь решения этой проблемы:
if os.path.exists(filename): os.remove(filename)Если файл filename создается другим потоком между вызовами os.path.exists и os.remove, то он не будет удален, что в свою очередь, может создать проблему для программы и даже дыру в безопасности.
По этой причине более правильным подходом будет попробовать удалить файл и проигнорировать ошибку, если окажется, что он не существует. Этот подход известен как Easier to Ask Forgiveness than to get Permission (Проще Попросить Прощения, чем Спросить Разрешения, EAFP). Правильный код будет выглядеть так (и будет работать и на POSIX и на Windows):
try: os.remove(filename) except OSError as e: if e.errno != errno.ENOENT: raiseили
try: os.remove(filename) except EnvironmentError as e: if e.errno != errno.ENOENT: raiseПравда, в данном случае приходится больше набирать и помнить различные мнемонические значения модуля errno. Всё это требует больше умственного труда и быстро вызывает утомление. Логично, что многие программисты вместо этого предпочтут следующий код, который, однако, перехватывает исключения слишком широко:
try: os.remove(filename) except OSError: passos.remove может возбуждать OSError не только когда файла не существует, но и в ряде других случаев — например, когда имя файла указывает на директорию, или текущий процесс не имеет прав на удаление файла; любой из этих случаев говорит об ошибке в логике программы и потому не должен оставаться незамеченным. Что на самом деле нужно программистам, так это
try: os.remove(filename) except FileNotFoundError: passСтратегия обеспечения совместимости
Реструктуризация иерархии исключений очевидно изменит семантическое значение части существующего кода. Поскольку улучшение существующей ситуации невозможно без изменения этой семантики, необходимо определить узкий тип совместимости, который мы назовем "полезной совместимостью".
Для этого, во-первых, мы должны объяснить, что мы имеем ввиду под осторожным или небрежным перехватом исключений. Небрежный, он же "наивный" код — это код, который вслепую перехватывает любое исключение типа OSError, IOError, socket.error, mmap.error, WindowsError, select.error без проверки атрибута errno. Причина такого названия в том, что эти типы исключений слишком широки, чтобы обозначать что-либо. Любое из них может быть возбуждено при различных условиях, например: плохой дескриптор файла (что обычно обозначает программную ошибку), сокет без соединения (то же самое), таймаут сокета, несоответствие типа файла, неверный аргумент, ошибка передачи, недостаточные права, несуществующая директория, переполнение файловой системы и т.д.
(более того, использование некоторых этих исключений не постоянно, смотрите Приложение А, где приведен обзор модуля select, который возбуждает различные исключения в зависимости от реализации)
Осторожный код — это код, который при перехвате любого из вышеупомянутых исключений, проверяет его errno атрибут, чтобы выяснить условия возбуждения исключения и обработать его в соответствии с этим.
Теперь мы можем дать определение полезной совместимости:
- полезная совместимость не делает перехват исключений имеющимся кодом уже, но может сделать его шире для небрежного кода. В следующем примере все исключения, перехватывавшиеся до этого PEP будут перехватываться и после его внедрения, однако для пропуска исключений это утверждение не верно (так как соединение OSError, IOError и других типов подразумевает, что выражение except будет забрасывать более широкую сеть):
try: ... os.remove(filename) ... except OSError: pass
- полезная совместимость не изменяет поведение осторожного кода. В следующем примере ошибки будут пропускаться и перехватываться вне зависимости от внедрения этого PEP:
try: os.remove(filename) except OSError as e: if e.errno != errno.ENOENT: raise
Осторожный код, с другой стороны, не должен пострадать, так как одна из целей этого PEP — облегчить написание такого кода.
Шаг 1: объединение типов исключений
Первый шаг на нашем пути — соединение существующих типов исключений. Предложены следующий изменения:
- добавление псевдонима OSError для socket.error и select.error
- добавление псевдонима OSError для mmap.error
- добавление псевдонима OSError для WindowsError и VMSError
- добавление псевдонима OSError для IOError
- включение EnvironmentError в OSError
Каждое из этих изменений может быть принято или отвергнуто по отдельности, но, очевидно, что наилучший результат будет достигнут после принятия всех предложенных изменений.
В таком случае иерархия IO исключений будет такой:
+-- OSError (replacing IOError, WindowsError, EnvironmentError, etc.) +-- io.BlockingIOError +-- io.UnsupportedOperation (also inherits from ValueError) +-- socket.gaierror +-- socket.herror +-- socket.timeoutОбъяснение правомерности
Этот шаг не только позволит получить более простой ландшафт исключений, как говорилось в Предложении, но и позволит достичь более полного осуществления второго шага (см. пункт Предварительные требования)
Причина сохранения OSError в качестве имени для всех исключений, зависящих от операционной системы, в том, что оно более общее, чем IOError. EnvironmentError гораздо дольше набирать да и менее известен.
Обзор в Приложении Б показывает, что IOError на данный момент является преобладающим исключением в стандартной библиотеке. Для стороннего кода Google Code Search показывает, что IOError в десять раз популярнее, чем EnvironmentError и в три раза чем OSError. И тем не менее, при всём уважении к IOError, меньшая известность OSError не является препятствием для нашего предложения.
Атрибуты исключений
Так как WindowsError включается в OSError, последний тип получает winerror атрибут в Windows. Он устанавливается в None в тех ситуациях, когда он не имеет смысла, как уже происходит с errno, filename и sterror атрибутами (например, когда OSError возбуждается напрямую кодом)
Неодобряемые имена
Следующие параграфы содержат краткий обзор возможных стратегий введения неодобряемых имен исключений.На данный момент все имена продолжают существовать как псевдонимы. Это решение может быть пересмотрено в релизе 4,0
встроенные исключения
Отказ от старых встроенных исключений не может быть проведена топорно, просто перехватывая все их имена во встроенном пространстве имён, так как это критично отразится на производительности. Мы так же не можем работать на объектном уровне, так как неодобряемые имена будут псевдонимами для одобряемых объектов (не спрашивайте меня, что тут имелось ввиду:) )
Решением может быть распознавание этих имен в процессе компиляции и выдача отдельного опкода LOAD_OLD_GLOBAL вместо обычного LOAD_GLOBAL. Этот опкод перехватывает вывод DeprecationWarning (или PendingDeprecationWarning, в зависимости от политики, определённой в результате) в случае, если имя не существует в глобальном пространстве имён, но только если оно является встроенным. Этого достаточно чтобы избежать ложных срабатываний (в случае если кто-то определяет свой собственный OSError в модуле), а ошибки будут достаточно редки (например, если кто-то получает доступ к OSError через thebuiltins модуль, а не напрямую)
исключения, определяемые в модулях
Изложенный выше подход не может быть просто использован в данном случае, так как он требует специальной обертки для модулей во время их компиляции. Однако, эти имена менее видимы (они не попадают во встроенное пространство имён) и так же менее известны, так что мы можем сохранить им жизнь в их собственных пространствах имён.
Шаг 2: определение дополнительных подклассов
Второй шаг нашего пути — расширение иерархии путём определения подклассов, которые и будут возбуждаться, вместо родительского класса с некоторым errno. Конкретные значения errno — предмет для обсуждения, но обзор существующих исключений (см Приложение А) поможет нам предложить разумный набор всех значений. Альтернативный вариант выдачи каждому errno мнемонического имени выглядит глупым, бессмысленным и лишь загрязнит корневое пространство имён.
Более того, в некоторых случаях, различные errno соответствуют одному и тому же подклассу. Например, EAGAIN, EALREADY, EWOULDBLOCK и EINPROGRESS используются для того, чтобы сообщить, что операция над не блокируемым сокетом будет заблокирована (и потому надо попробовать ещё раз позже). Поэтому все они могут возбуждать один и тот же подкласс и оставить на усмотрение пользователя проверку errno (смотрите ниже "атрибуты исключений").
Предварительные требования
Шаг 1 — необязательное предварительное требования для этого шага.
Предварительное требование, так как некоторые errno могут относиться к различным классам исключений: например, ENOENT может быть присоединён как к OSError так и к IOError, в зависимости от контекста. Если мы не хотим нарушать полезную совместимость, мы не можем допустить, чтобы OSError или IOError не подпадал под соответствие выражению except там, где сейчас оно срабатывает.
Необязательное, так как мы можем выполнить второй шаг и без объединения соответствующих классов. Например, ENOENT может возбуждать гипотетическое исключение FileNotFoundError в случае, если перед этим было возбуждено IOError, или OSError во всех остальных случаях.
Зависимость от первого шага может быть в принципе полностью устранена, если новые подклассы будут иметь множественное наследование, чтобы соответствовать всем существующим подклассам (или, как минимум, OSError и IOError, как наиболее распространённым). Но такой вариант сделает иерархию более сложной и потому тяжелее воспринимаемым пользователями.
Новые классы исключений
Следующий предварительный список подклассов вместе с их описанием и соответствующими errno является объектом для обсуждения:
- FileExistsError: попытка создать файл или директорию, которые уже существуют (EEXIST)
- FileNotFoundError: для всех случаев, когда требуемый файл или директория не существуют (ENOENT)
- IsADirectoryError: операции над файлами (open(), os.remove()...), применяемые к директории (EISDIR)
- NotADirectoryError: операции над директорией, применяемые к чему-либо другому (ENOTDIR)
- PermissionError: попытка выполнить операцию без адекватный прав доступа — например, прав файловой системы (EACCES, EPERM)
- BlockingIOError: операция, которая должна заблокировать объект (например, сокет) устанавливается для неблокируемой операции (EAGAIN, EALREADY, EWOULDBLOCK, EINPROGRESS); на данный момент эту роль выполняет io.BlockingIOError с расширенной ролью
- BrokenPipeError: попытка записать в канал, закрытый на другом конце, или попытка записать в сокет, который закрыт для записи (EPIPE, ESHUTDOWN)
- InterruptedError: системный вызов прерван входящим сигналом (EINTR)
- ConnectionAbortedError: попытка прервать соединение узлом (ECONNABORTED)
- ConnectionRefusedError: соединение отклонено узлом (ECONNREFUSED)
- ConnectionResetError: соединение переустановлено узлом (ECONNRESET)
- TimeoutError: таймаут соединения (ETIMEDOUT); оно может быть использовано как стандартное таймаут исключение, заменив socket.timeout и будучи полезным для других ситуаций с таймаутом (например, для Lock.acquire())
- ChildProcessError: операция над дочерним процессом не удалась (ECHILD), очень часто возбуждается семейством функций wait()
- ProcessLookupError: данный процесс (идентифицируемый, например, по process id) не существует (ESRCH)
- ConnectionError: базовый класс для ConnectionAbortedError, ConnectionRefusedError и ConnectionResetError
+-- BlockingIOError EAGAIN, EALREADY, EWOULDBLOCK, EINPROGRESS +-- ChildProcessError ECHILD +-- ConnectionError +-- BrokenPipeError EPIPE, ESHUTDOWN +-- ConnectionAbortedError ECONNABORTED +-- ConnectionRefusedError ECONNREFUSED +-- ConnectionResetError ECONNRESET +-- FileExistsError EEXIST +-- FileNotFoundError ENOENT +-- InterruptedError EINTR +-- IsADirectoryError EISDIR +-- NotADirectoryError ENOTDIR +-- PermissionError EACCES, EPERM +-- ProcessLookupError ESRCH +-- TimeoutError ETIMEDOUTИмена
По поводу имён могут возникать различные споры. В частности, должны ли все имена исключений заканчиваться на Error. За — согласованность с остальной иерархией исключений, против — краткость (особенно для длинных имён, таких как ConnectionAbortedError).
Атрибуты исключений
Для того, чтобы сохранить полезную совместимость, данные подклассы должны устанавливать адекватные значения различных атрибутов исключений, в зависимости от суперкласса (например, errno, filename и, возможно, winerror).
Реализация
Так как предполагается, что подклассы будут возбуждаться на основе значения errno, то изменения в модулях расширения (стандартных или сторонних) если и потребуются, то будут минимальны.
Первая возможность — адаптировать семейство функций PyErr_SetFromErrno() (PyErr_SetFromWindowsErrno() под Windows) для возбуждения соответствующего подкласса OSError. Однако, это не покроет код на Python, который возбуждает OSError напрямую, используя такой код (из Lib/tempfile.py):
raise IOError(_errno.EEXIST, "No usable temporary file name found")Вторая возможность, предложенная Marc-Andre Lemburg — адаптировать OSError.__new__ для создания экземпляров соответствующего подкласса. Такая реализация покроет так же и код, приведённый выше.
Возможные возражения
Загрязнение пространства имён
Более тонкая иерархия делает корневое (или встроенное) пространство имён более заполненным. Однако, разрастание пространства имён будет ограниченным, так как:
- предлагается лишь несколько дополнительных классов
- хотя стандартные исключения живут в корневом пространстве имён, они визуально отличаются от других встроенных имён, тем что используют ВерблюжийРегистр в то время как другие встроенные имена пишутся строчными буквами (кроме True, False, None, Ellipsis и NotImplemented)
Ранние обсуждения
Хотя это и первое формальное предложение, тем не менее данная идея уже получала поддержку в прошлом и касательно детализации иерархии исключений и касательно слияния OSError и IOError.
Удаление WindowsError уже обсуждалось, но было отклонено в рамках другого PEP, однако и там пришли к консенсусу, что выделение этого типа из OSError не было осмысленным, что говорит в пользу слияния с OSError.
Реализации
Эталонная реализация была включена в Python 3.3. Разработка проводилась на http://hg.python.org/features/pep-3151/ в ветке pep-3151, багтрекер находился тут. Реализация была успешно протестирована на различных системах: Linux, Windows, OpenIndiana и FreeBSD.
Одним из источников проблем были соответствующие конструкторы OSError и WindowsError, которые оказались несовместимыми. Решили это сохранением сигнатуры OSError и добавлением четвёртого опционного аргумента для передачи Windows error code (отличающегося от POSIX errno). В четвёртом аргументе сохранен winerror, соответствующий POSIX errno. PyErr_SetFromWindowsErr* функции были адаптированны для правильного вызова конструктора.
Есть одна маленькая сложность когда функции PyErr_SetFromWindowsErr* вызываются из OSError а не из WindowsError: в атрибуте errno объекта исключения сохранится Windows error code (например, 109 для ERROR_BROKEN_PIPE) а не его POSIX аналог (32 для EPIPE), как это делается сейчас. Для кодов ошибок не связанных с сокетом это касается только частного модуля _multiprocessing, так что нет причин для беспокойства.
Заметка
Для ошибок в сокете POSIX errno, как показано в модуле errno, численно равно Windows Socket error code, возвращаемому системным вызовом WSAGetLastError:
>>> errno.EWOULDBLOCK 10035 >>> errno.WSAEWOULDBLOCK 10035Возможные альтернативы
Соответствие шаблонам
Альтернативная возможность представлена продвинутым соответствием шаблонам при перехвате исключений. Например:
try: os.remove(filename) except OSError as e if e.errno == errno.ENOENT: passНекоторые проблемы при таком решении:
- тут используется новый синтаксис, что воспринимается автором как более серьезное исключение, чем реструктуризация иерархии
- это не сокращает количество кода, который требуется набрать
- это не спасает программиста от необходимости помнить мнемонику errno
Данный PEP игнорирует EOFError, что сигнализирует о прерванном вводе из потока для различных протоколов и реализаций форматов файлов (например, GzipFile). EOFError не связан с IOError или OSError, это логическая ошибка, возбуждаемая на высоком уровне.
Данный PEP так же игнорирует SSLError, возбуждаемый модулем ssl для распространения ошибок, обозначенных в библиотеке OpenSSL. В идеале, SSLError выиграл бы от подобного подхода, но для него требуется отдельная обработка, так как он определяет свои собственные константы типов ошибок (ssl.SSL_ERROR_WANT_READ и тд.). В Python 3.2 SSLError уже заменён socket.timeout в случае таймаута сокета (см проблему 10272)
И, наконец, судьба socket.gaierror и socket.herror ещё не определена. Хотя они заслуживают менее таинственных имён, это может быть сделано вне зависимости от пересмотра иерархии исключений.
Комментариев нет:
Отправить комментарий