Главная страница
Навигация по странице:

  • 23.1. «Посмотри, прежде чем прыгнуть»

  • 23.2. «Проще просить прощения, чем разрешения»

  • 23.3. Несколько возможных исключений

  • 23.4. finally

  • 23.5. Секция else

  • 23.6. Выдача исключений

  • 23.7. Упаковка исключений

  • Как устроен Python. Как устроен Python. Харрисон. Харрисон Мэтт


    Скачать 5.41 Mb.
    НазваниеХаррисон Мэтт
    АнкорКак устроен Python
    Дата05.02.2022
    Размер5.41 Mb.
    Формат файлаpdf
    Имя файлаКак устроен Python. Харрисон.pdf
    ТипДокументы
    #352210
    страница18 из 21
    1   ...   13   14   15   16   17   18   19   20   21
    220
    Глава 23. Исключения
    Допустим, файл содержит следующий код:
    def err():
    1/0
    def start():
    return middle()
    def middle():
    return more()
    def more():
    err()
    При попытке выполнить его вы получите следующую трассировку:
    Traceback (most recent call last):
    File "/tmp/err.py", line 13, in
    start()
    File "/tmp/err.py", line 5, in start return middle()
    File "/tmp/err.py", line 8, in middle return more()
    File "/tmp/err.py", line 11, in more err()
    File "/tmp/err.py", line 2, in err
    1/0
    ZeroDivisionError: division by zero
    Трассировки проще всего читаются в обратном направлении: нач- ните снизу, найдите ошибку и посмотрите, где она произошла. При перемещении вверх по трассировке вы фактически поднимаетесь по цепочке вызовов. Это поможет вам выяснить, что происходит в ваших программах.
    23.1. «Посмотри, прежде чем прыгнуть»
    Предположим, у вас имеется программа, которая выполняет деление.
    В зависимости от того, как написан код, в какой-то момент она может по- пытаться выполнить деление на ноль. Программисты обычно применяют

    23.2. «Проще просить прощения, чем разрешения»
    221
    два стиля обработки исключений. Первый стиль называется LBYL (Look
    Before You Leap, то есть «Посмотри, прежде чем прыгнуть»). Его суть в том, чтобы проверить исключительную ситуацию перед выполнением действия. В нашем случае программа проверяет делитель на ноль. Если делитель отличен от нуля, программа может выполнить деление; если нет — операцию следует пропустить.
    В Python стиль LBYL можно реализовать с помощью команд if
    :
    >>> numerator = 10
    >>> divisor = 0
    >>> if divisor != 0:
    ... result = numerator / divisor
    ... else:
    ... result = None
    ПРИМЕЧАНИЕ
    Принцип LBYL не дает гарантии успеха. Даже если вы проверите, что файл существует, прежде чем открывать его, это не означает, что он будет существо- вать потом. В многопоточных средах такая ситуация называется условием
    гонки (race condition).
    ПРИМЕЧАНИЕ
    Значение
    None используется для представления неопределенного состояния.
    Это одна из распространенных идиом мира Python. Будьте внимательны и не пытайтесь вызывать методы для переменной, которой присвоено значение
    None
    , — это приведет к выдаче исключения.
    23.2. «Проще просить прощения,
    чем разрешения»
    Другой стиль обработки исключений обычно обозначается сокращением
    EAFP (Easier to Ask for Forgiveness than Permission, то есть «Проще попро- сить прощения, чем разрешения»). Если операция завершается неудачей, исключение будет перехвачено в блоке исключения.
    Конструкция try...except предоставляет механизм для перехвата ис- ключительных ситуаций в Python:

    222
    Глава 23. Исключения
    >>> numerator = 10
    >>> divisor = 0
    >>> try:
    ... result = numerator / divisor
    ... except ZeroDivisionError as e:
    ... result = None
    Конструкция try создает блок после ключевого слова try
    (на что указы- вает двоеточие и отступы). Внутри блока try находятся команды, которые могут выдавать исключения. Если при выполнении команд действитель- но произойдет исключение, Python ищет блок except
    , который перехва-
    тывает это исключение (или исключение его родительского класса).
    В приведенном коде блок except перехватывает исключения, являющи- еся экземплярами класса
    ZeroDivisionError
    (или его субклассов). Когда в блоке try происходит указанное исключение, выполняется блок except
    , а результату присваивается
    None
    Обратите внимание: в строке except ZeroDivisionError as e:
    в последней позиции стоит двоеточие. Эта часть не обязательна: если она присутствует, то e
    (или другое имя переменной, которое вы выбрали) будет указывать на экземпляр исключения
    ZeroDivisionError
    . Вы можете проанализировать объект исключения, нередко в нем содержится более подробная информация. Переменная e
    указывает на активное исключе-
    ние. Если вы не включите секцию as e
    в конце команды except
    , активное исключение в программе все равно будет, но вы не сможете обратиться к его экземпляру.
    СОВЕТ
    Постарайтесь ограничить область действия блока try
    . Вместо того чтобы включать весь код функции в блок try
    , включите только ту строку, в которой может произойти ошибка.
    Так как стиль LBYL не гарантирует успешного предотвращения ошибок, обычно разработчики Python предпочитают стиль EAFP. Несколько практических правил обработки исключений:

    23.3. Несколько возможных исключений
    223
    
    Обрабатывайте те ошибки, которые вы можете обработать и которые можно ожидать в программе.
    
    Не подавляйте те исключения, которые вы не можете обработать, и те, которые вряд ли возникнут в вашей программе.
    
    Используйте глобальный обработчик исключений для корректной обработки непредвиденных ошибок.
    СОВЕТ
    Если вы пишете серверное приложение, которое должно работать непре- рывно, один из возможных способов показан ниже (функции process_input и log_error не существуют и приведены исключительно в демонстрационных целях):
    while 1:
    try:
    result = process_input()
    except Exception as e:
    log_error(e)
    23.3. Несколько возможных исключений
    Если есть сразу несколько исключений, которые должны обрабатываться вашим кодом, перечислите их в нескольких командах except
    , следующих друг за другом:
    try:
    some_function()
    except ZeroDivisionError as e:
    # Обработка конкретного исключения except Exception as e:
    # Обработка других исключений
    В этом примере, когда some_function выдает исключение, интерпретатор сначала проверяет, соответствует ли оно ошибке класса
    ZeroDivisionError или его субкласса. Если условие не выполняется, код проверяет, отно- сится ли исключение к субклассу
    Exception
    . После входа в блок except
    Python уже не проверяет последующие блоки.

    224
    Глава 23. Исключения
    Если исключение не обработано цепочкой, оно должно быть обработано кодом где-то в стеке вызовов. Если исключение так и остается необрабо- танным, Python прекращает выполнение и выводит трассировку стека.
    Пример обработки нескольких исключений встречается в стандартной библиотеке. Модуль argparse из стандартной библиотеки предоставляет простой механизм разбора параметров командной строки. Он позволяет указать тип некоторых параметров — например, целых чисел или файлов
    (все параметры поступают в строковой форме). В методе
    ._get_value встречаются примеры использования нескольких секций except
    . В за- висимости от типа инициированного исключения выдаются разные сообщения об ошибках:
    def _get_value(self, action, arg_string):
    type_func = self._registry_get('type', action.type, action.type)
    if not callable(type_func):
    msg = _('%r is not callable')
    raise ArgumentError(action, msg % type_func)
    # преобразование значения к соответствующему типу try:
    result = type_func(arg_string)
    # ArgumentTypeError - признак ошибки except ArgumentTypeError:
    name = getattr(action.type, '__name__', repr(action.type))
    msg = str(_sys.exc_info()[1])
    raise ArgumentError(action, msg)
    # TypeError и ValueErrors тоже являются признаками ошибок except (TypeError, ValueError):
    name = getattr(action.type, '__name__', repr(action.type))
    args = {'type': name, 'value': arg_string}
    msg = _('invalid %(type)s value: %(value)r')
    raise ArgumentError(action, msg % args)
    # возвращается преобразованное значение return result

    23.4. finally
    225
    ПРИМЕЧАНИЕ
    Этот пример показывает, что одна команда except может перехватывать сра- зу несколько типов исключений, для чего следует передать кортеж классов исключений:
    except (TypeError, ValueError):
    ПРИМЕЧАНИЕ
    Этот пример также демонстрирует старый стиль форматирования строк с использованием оператора
    %
    . Строки msg = _('invalid %(type)s value: %(value)r')
    raise ArgumentError(action, msg % args)
    в современном стиле записываются так:
    msg = _('invalid {type!s} value: {value!r}')
    raise ArgumentError(action, msg.format(**args))
    23.4. finally
    Еще одна конструкция обработки ошибок — секция finally
    . Эта команда используется для определения кода, который будет выполняться всег- да — независимо от того, возникло исключение или нет.
    Блок finally выполняется всегда. Если исключение было обработано, то блок finally будет выполнен после обработки. Если исключение не было обработано, то блок finally выполняется, а исключение иниции- руется заново:
    try:
    some_function()
    except Exception as e:
    # Обработка ошибок finally:
    # Завершающие действия

    226
    Глава 23. Исключения
    Обычно секция finally используется для освобождения внешних ре- сурсов: файлов, сетевых подключений, баз данных и т. д. Эти ресурсы должны освобождаться независимо от того, была операция выполнена успешно или нет.
    Пример из модуля timeit
    , входящего в стандартную библиотеку, по- могает понять полезность команды finally
    . Модуль timeit позволяет разработчику проводить хронометраж кода. В частности, во время про- ведения хронометража модуль приказывает уборщику мусора прекратить работу. Однако после завершения хронометража уборщик мусора должен быть снова включен независимо от того, был ли хронометраж завершен успешно или произошла ошибка.
    Ниже приведен метод timeit
    , выполняющий хронометраж. Он проверяет, включен ли уборщик мусора, при необходимости отключает его, выпол- няет хронометражный код и, наконец, заново включает уборку мусора, если она была включена до этого:
    def timeit(self, number=default_number):
    """Хронометраж 'number' выполнений основной команды.
    Для повышения точности команда подготовки выполняется однократно, после чего время, необходимое для многократного выполнения основной команды, возвращается как вещественное число (в секундах). Аргумент определяет количество выполнений цикла (по умолчанию один миллион).
    Основная команда, команда подготовки и используемая функция таймера передаются конструктору.
    """
    it = itertools.repeat(None, number)
    gcold = gc.isenabled()
    gc.disable()
    try:
    timing = self.inner(it, self.timer)
    finally:
    if gcold:
    gc.enable()
    return timing
    При вызове self.inner может произойти исключение, но, поскольку стандартная библиотека использует finally
    , уборка мусора всегда будет

    23.5. Секция else
    227
    включаться независимо от исключения (если логическая переменная gcold истинна).
    ПРИМЕЧАНИЕ
    В этой книге менеджеры контекста не рассматриваются, но, чтобы подгото- вить вас к будущей карьере эксперта Python, мы приведем небольшой совет.
    Комбинация try/finally в Python считается кодом «с душком». Опытные программисты Python в таких случаях применяют менеджер контекста.
    Включите эту тему в список вопросов, которые вам будет нужно изучить после освоения базовых возможностей Python.
    23.5. Секция else
    Необязательная секция else в команде try выполняется в том случае, если не было выдано никаких исключений. Она должна следовать за всеми секциями except и выполняется перед блоком finally
    . Простой пример:
    >>> try:
    ... print('hi')
    ... except Exception as e:
    ... print('Error')
    ... else:
    ... print('Success')
    ... finally:
    ... print('at last')
    hi
    Success at last
    Ниже приведен пример из модуля heapq стандартной библиотеки. Как следует из комментариев, существует ускоренное решение, если число запрашиваемых значений превышает размер кучи. Тем не менее, если при попытке получения размера кучи произойдет ошибка, в коде вызывается pass
    . В результате ошибка игнорируется, а выполнение продолжается по более медленному варианту. Если ошибки не было, можно пойти по пути else и выбрать быстрый путь, если n
    превышает размер кучи:

    228
    Глава 23. Исключения def nsmallest(n, iterable, key=None):
    # ....
    # Если n>=size, быстрее использовать sorted()
    try:
    size = len(iterable)
    except (TypeError, AttributeError) as e:
    pass else:
    if n >= size:
    return sorted(iterable, key=key)[:n]
    # Часть кода пропущена .... Использовать более медленный способ
    23.6. Выдача исключений
    Помимо перехвата исключений, Python также дает возможность вы- давать исключения в коде (то есть инициировать их). Вспомните, что
    Дзен Python рекомендует явно выражать свои намерения и бороться с искушением что-либо предполагать. Если функции передается невер- ный ввод и вы знаете, что не сможете с ним справиться, можно выдать исключение. Исключения являются субклассами класса
    BaseException
    , а для их выдачи используется команда raise
    :
    raise BaseException('Program failed')
    Обычно в программе выдается не обобщенное исключение класса
    BaseException
    , а исключение одного из его субклассов — уже готовых или определенных разработчиком. В другом распространенном варианте команда raise используется сама по себе. Вспомните, что внутри команды except существует так называемое активное исключение. В таком случае можно обойтись минимальной командой raise
    . Эта команда позволяет обработать исключение, а потом заново выдать исходное исключение.
    При попытке выполнения кода except (TypeError, AttributeError) as e:
    log('Hit an exception')
    raise e вам удастся успешно инициировать исходное исключение, но трассиров- ка стека будет показывать, что исходное исключение теперь произошло в строке с командой raise e
    , а не там, где оно произошло сначала. У про-

    23.7. Упаковка исключений
    229
    блемы есть два решения. Первое — использование минимальной команды raise
    . Второе — использование сцепленных исключений (см. далее).
    Рассмотрим пример из модуля configparser стандартной библиотеки.
    Этот модуль обеспечивает чтение и создание INI-файлов. INI-файлы обычно используются для настройки конфигурации; они были по- пулярны до появления форматов JSON и YAML. Метод
    .read_dict пытается прочитать конфигурацию из словаря. Если экземпляр на- ходится в «жестком» режиме, при попытке добавления одной и той же секции более одного раза произойдет исключение. Если «жесткий» режим не включен, метод допускает наличие дубликатов ключей; ис- пользуется последний ключ. Часть метода, демонстрирующая вариант с минимальной командой raise
    :
    def read_dict(self, dictionary, source=''):
    elements_added = set()
    for section, keys in dictionary.items():
    section = str(section)
    try:
    self.add_section(section)
    except (DuplicateSectionError, ValueError):
    if self._strict and section in elements_added:
    raise elements_added.add(section)
    # Часть кода пропущена ....
    Если дубликат добавляется в «жестком» режиме, трассировка стека пока- жет ошибку в методе
    .add_section
    , потому что она произошла именно там.
    23.7. Упаковка исключений
    В Python 3 появилась новая возможность, сходная с минимальной коман- дой raise
    ; она описана в «PEP 3134 — Exception Chaining and Embedded
    Tracebacks». При обработке исключения в коде обработки может про- изойти другое исключение. В подобных ситуациях бывает полезно знать об обоих исключениях.
    Ниже приведен пример кода. У функции divide_work могут возникнуть проблемы с делением на 0. Вы можете перехватить эту ошибку и зареги- стрировать ее в журнале. Предположим, функция регистрации обращает-

    230
    Глава 23. Исключения ся к облачному сервису, который в настоящее время недоступен (чтобы смоделировать эту ситуацию, мы заставим log выдать исключение):
    >>> def log(msg):
    ... raise SystemError("Logging not up")
    >>> def divide_work(x, y):
    ... try:
    ... return x/y
    ... except ZeroDivisionError as ex:
    ... log("System is down")
    Если при вызове divide_work передать 5 и 0, Python выведет информацию о двух ошибках,
    ZeroDivisionError и
    SystemError
    . Ошибка
    SystemError будет выведена последней, потому что она произошла последней:
    >>> divide_work(5, 0)
    Traceback (most recent call last):
    File "begpy.py", line 3, in divide_work return x/y
    ZeroDivisionError: division by zero
    During handling of the above exception, another exception occurred:
    Traceback (most recent call last):
    File "begpy.py", line 1, in
    divide_work(5, 0)
    File "begpy.py", line 5, in divide_work log("System is down")
    File "begpy.py", line 2, in log raise SystemError("Logging not up")
    SystemError: Logging not up
    Предположим, облачный сервис журнала заработал (функция log уже не выдает ошибку). Если вы хотите изменить тип
    ZeroDivisionError в divide_work на
    ArithmeticError
    , используйте синтаксис, описанный в PEP 3134. Для этого можно воспользоваться синтаксисом raise...
    from
    :
    >>> def log(msg):
    ... print(msg)
    >>> def divide_work(x, y):
    ... try:
    ... return x/y

    23.7. Упаковка исключений
    231
    ... except ZeroDivisionError as ex:
    ... log("System is down")
    ... raise ArithmeticError() from ex
    Теперь вы видите два исключения: исходное
    ZeroDivisionError и ис- ключение
    ArithmeticError
    , которое уже не скрывается
    ZeroDivisionError
    :
    >>> divide_work(3, 0)
    Traceback (most recent call last):
    File "begpy.py", line 3, in divide_work return x/y
    ZeroDivisionError: division by zero
    Во время обработки вышеуказанного исключения произошло другое исключение:
    Traceback (most recent call last):
    File "begpy.py", line 1, in
    divide_work(3, 0)
    File "begpy.py", line 6, in divide_work raise ArithmeticError() from ex
    ArithmeticError
    Если вы хотите подавить исходное исключение
    ZeroDivisionError
    , ис- пользуйте следующий код (см. «PEP 0409 — Suppressing exception context»):
    >>> def divide_work(x, y):
    ... try:
    ... return x/y
    ... except ZeroDivisionError as ex:
    ... log("System is down")
    ... raise ArithmeticError() from None
    Теперь видна только внешняя ошибка
    ArithmeticError
    :
    >>> divide_work(3, 0)
    Traceback (most recent call last):
    File "begpy.py", line 1, in
    divide_work(3, 0)
    File "begpy.py", line 6, in divide_work raise ArithmeticError() from None
    ArithmeticError

    1   ...   13   14   15   16   17   18   19   20   21


    написать администратору сайта