Рубрики
Без рубрики

Написание и использование пользовательских исключений в Python

Вы когда-нибудь сталкивались с обратной связью при кодировании на Python? Узнайте, как создавать и использовать пользовательские исключения.

Автор оригинала: Sheena.

В этом уроке мы рассмотрим “что” и “почему” исключений в Python, а затем расскажем вам о процессе создания и использования ваших собственных типов исключений. И что еще более важно, когда этого не делать. Наслаждайтесь!

Что такое исключение?

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

>>> l = [1,2,3]               #1
>>> l['apples']               #2
Traceback (most recent call last):
  File "", line 1, in 
TypeError: list indices must be integers, not str
>>> l[4]                      #3
Traceback (most recent call last):
  File "", line 1, in 
IndexError: list index out of range
>>> l[1]                      #4
2

Итак, вот что мы только что сделали. В строке 1 мы составили список из трех элементов. Строка 2 пытается получить доступ к элементу в индексе 'яблоки' . Теперь, поскольку это действительно не круто, Питон жалуется. Он делает это, поднимая Ошибка типа . TypeError является своего рода Исключением . Когда Исключение возникает и, но не попадает в ловушку (мы вернемся к этому позже), он заканчивает тем, что печатает обратную трассировку к выходу ошибки. В случае консоли Python вывод ошибок-это просто консоль. Сообщение об обратном отслеживании дает некоторую информацию о фактической ошибке и дает некоторые подробности о том, как мы добрались до точки, где ошибка действительно произошла. Если это последнее предложение сбивало с толку, не волнуйтесь, это должно проясниться в следующем примере.

TypeError жалуется, что он ожидал целое число, а не строку. Это кажется довольно разумной реакцией. Поэтому в строке 3 мы даем ему целое число. К сожалению, единственными индексами, доступными для l , являются 0,1 и 2, но мы пытаемся получить доступ к l[4] . Естественно, это тоже не круто. Поэтому Python снова жалуется, вызывая IndexError и печатая соответствующую обратную трассировку.

Наконец, мы делаем что-то разумное и получаем доступ l[1] . Это, по сравнению с другими нашими попытками, круто. l[1] имеет значение 2 .

Здорово. Итак, чему же мы научились?

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

Обратная связь

Вот более интересный пример, просто чтобы продемонстрировать немного больше о трассировке и о том, почему это исключение также круто:

>>> def f1(x):
...     assert x == 1
... 
>>> def f2(x):
...     f1(x)
...  
>>> def f3(x):
...     f2(x)
...     not_cool
... 

До сих пор в приведенном выше коде не произошло ничего удивительного. Мы сделали три функции такими, что f3 вызовы f2 вызовы f1 . А теперь давайте что-нибудь с ними сделаем:

>>> f1(1)
>>> f1(2)
Traceback (most recent call last):
  File "", line 1, in 
  File "", line 2, in f1
AssertionError

Здесь мы встречаем AssertionError . Это исключение возникает каждый раз, когда x принимает значение False . Довольно прямолинейно. В качестве примечания, утверждения, подобные приведенному выше, довольно полезны для “проверки здравомыслия” во время выполнения кода.

>>> f2(1)
>>> f2(2)
Traceback (most recent call last):
  File "", line 1, in 
  File "", line 2, in f2
  File "", line 2, in f1
AssertionError

На этот раз трассировка немного длиннее. Из этой трассировки вы можете видеть, что AssertionError был вызван в f1 , и что f1 был вызван f2 . У него даже есть номера строк. Номера строк сейчас не слишком полезны, потому что мы просто вводим данные в консоль. Однако написание и выполнение сложных программ на Python полностью в консоли не является обычной практикой; обычно вы будете вызывать функции, которые хранятся в файлах. Если такая функция вызывает исключение, то обратная трассировка поможет вам точно определить, в какой строке какого файла возникла ошибка.

>>> f3(1)
Traceback (most recent call last):
  File "", line 1, in 
  File "", line 3, in f3
NameError: global name 'not_cool' is not defined
>>> 

На этот раз мы вызываем f3 . Значение 1 передается в f1 , и оператор утверждения не вызывает ошибок. Затем программа возвращается к f3 , где оператор not_cool вызвал ошибку. Обратная трассировка не содержит никакой информации о f1 и f2 , поскольку эти функции уже выполнялись без ошибок.

>>> f3(2)
Traceback (most recent call last):
  File "", line 1, in 
  File "", line 2, in f3
  File "", line 2, in f2
  File "", line 2, in f1
AssertionError

На этот раз мы выполним f3 и дадим ему значение, которое вызовет ошибку Утверждения , вызванную f1 . Обратная связь описывает процесс.

Если у вас возникли какие-либо проблемы с пониманием того, как поток программы может быть определен по сообщению обратной трассировки, то, вероятно, вам стоит немного почитать о “стеках вызовов”.

Перехват Исключений

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

>>> try:
...     f1(1)
... except:
...     print "caught an exception"
... 

Аааа, и ничего не происходит. Отлично, не было никаких исключений.

>>> try:
...     f1(2)
... except:
...     print "caught an exception"
... 
caught an exception

На этот раз был один, и мы поймали мерзавца. Все, что вы помещаете в блок , кроме , будет выполняться только в том случае, если он поймает исключение. Но дело не только в этом:

>>> try:
...     f1(1)
... except:
...     print "caught an exception"
... else:
...    print "no exception"
... finally:
...    print "the end"
no exception
the end
>>>
>>> try:
...     f1(2)
... except:
...     print "caught an exception"
... else:
...    print "no exception"
... finally:
...    print "the end"
caught an exception
the end

Блок else выполняется только в том случае, если не будет поймано исключение. И наконец блок будет выполнен, несмотря ни на что.

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

Например:

>>> def foo(i):
...     l = [1,2,3]
...     try:
...         assert i >= 1
...         return l[i]
...     except TypeError,e:                                  ####A
...         print "dealing with TypeError"
...     except IndexError, e:                                ####B
...         print "dealing with IndexError"
...     except:                                                         ####C
...         print "oh dear"
...     finally:                                                           ####D
...         print "the end"
... 
>>> 
>>> foo('apples')
dealing with TypeError
the end
>>> 
>>> foo(-1)
oh dear
the end
>>> 
>>> foo(4)
dealing with IndexError
the end
>>> 
>>> foo(1)
the end
2

Всякий раз , когда мы вызываем foo , мы пытаемся вернуть запрошенный элемент списка, если он положительный. У нас есть 3 различных способа перехвата исключений. Если возникает исключение, то выполнение переходит к первому блоку except , который соответствует исключению. Другими словами, если возникает исключение, то Python сначала проверяет, является ли оно TypeError (A). Если это TypeError , затем он обработает этот кроме блока, прежде чем перейти к , наконец, блоку. Если это не TypeError , затем Python проверяет, является ли это IndexError (B) и т. Д.

Обратите внимание, что это означает, что порядок блоков кроме имеет значение. Давайте отредактируем foo , чтобы выглядеть так:

>>> def foo(i):
...     l = [1,2,3]
...     try:
...         assert i >= 1
...         return l[i]
...     except:                                                         ####C
...         print "oh dear"
...     except TypeError,e:                                  ####A
...         print "dealing with TypeError"
...     except IndexError, e:                                ####B
...         print "dealing with IndexError"
...     finally:                                                           ####D
...         print "the end"
... 

Хорошее эмпирическое правило состоит в том, чтобы ловить только исключения, которые вы готовы обрабатывать .

Намеренное Создание Исключений

Вы можете явно вызывать исключения двумя способами (ошибка в коде-это скорее неявный метод).

Первый способ-вызвать исключение, которое вы поймали. Например:

try:
    do_important_stuff()
except:
    import traceback
    s = traceback.format_exc()
    send_error_message_to_responsible_adult(s)
    raise 

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

def greet_person(sPersonName):
    """
    says hello
    """
    if sPersonName == "Robert":
        raise Exception("we don't like you, Robert")
    print "Hi there {0}".format(sPersonName)

Попробуйте поприветствовать несколько человек и посмотреть, что произойдет.

Исключения подклассов и другие Причудливые вещи

Поскольку исключения являются объектами и могут быть построены, имеет смысл, что мы можем подклассировать класс Exception . Или даже подклассы подклассов класса Exception .

class MyException(Exception):
    def __init__(self,*args,**kwargs):
        Exception.__init__(self,*args,**kwargs)


class MyIndexError(IndexError):
def __init__(self,*args,**kwargs):
        IndexError.__init__(self,*args,**kwargs)

Теперь вы можете создавать свои собственные исключения, как и любое другое исключение. Обратите внимание, что при создании операторов except наследование имеет значение. То есть оператор except , направленный на IndexError , также поймает My IndexError . Это происходит потому, что My IndexError ЯВЛЯЕТСЯ IndexError . И наоборот, если у вас есть блок except , направленный на MyIndexError , то он НЕ поймает IndexError . Это происходит потому, что IndexError НЕ ЯВЛЯЕТСЯ Моим IndexError .

Так что это достаточно просто. Но зачем вам это нужно? Ну, на самом деле лучше всего избегать подобных вещей. современные версии Python уже имеют богатый набор исключений, поэтому часто не стоит создавать больше вещей. Если вы хотите создать Mayindexerror , то спросите себя, подойдет ли обычный IndexError . Если это не поможет, то, возможно, оно того стоит.

Например, если контекст, в котором было вызвано исключение, имеет дополнительное значение, то, возможно, стоит сохранить этот контекст в исключении:

class MyExceptionWithContext(Exception):
    def __init___(self,dErrorArguments):
        Exception.__init__(self,"my exception was raised with arguments {0}".format(dErrArguments))
        self.dErrorArguments = dErrorArguements
        
def do_stuff(a,b,c):
   if some_complicated_thing(a,b,c):
       raise MyExceptionWithContext({
                'a' : a,
                'b' : b,
                'c' : c,
       })
   else:
       return life_goes_on(a,b,c)

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

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

>>> class OhMyGoodnessExc(Exception):
...    def __init__(self):
...        Exception.__init__(self,"well, that rather badly didnt it?") 
... 
>>> raise OhMyGoodnessExc()
Traceback (most recent call last):
  File "", line 1, in 
__main__.OhMyGoodnessExc: well, that rather badly didnt it?
>>>
>>> raise OhMyGoodnessExc()
Traceback (most recent call last):
  File "", line 1, in 
__main__.OhMyGoodnessExc: well, that rather badly didnt it?

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

Это более легко и аккуратно достигается, просто заранее создав подходящее исключение и подняв его, когда вам нужно:

>>> oh_my_goodness = Exception("well, that rather badly didnt it?")
>>>
>>> raise oh_my_goodness
Traceback (most recent call last):
  File "", line 1, in 
Exception: well, that rather badly didnt it?
>>>
>>> raise oh_my_goodness
Traceback (most recent call last):
  File "", line 1, in 
Exception: well, that rather badly didnt it?

Здесь следует отметить, что исключение не нужно вызывать сразу после его создания. А когда сомневаешься, ПОЦЕЛУЙ ( K eep I t S imple, S tupid).

Вывод

Мы рассмотрели здесь много вопросов, в частности, что делают исключения и как и когда их ловить; как заставить их произойти и как создать свои собственные классы исключений. Мы также рассмотрели довольно много хорошей практики в отношении исключений. Вот небольшое резюме:

  1. Не все исключения создаются равными: если вы знаете, с каким классом исключений вы имеете дело, то будьте конкретны в том, что вы ловите
  2. Не ловите ничего, с чем вы не можете справиться
  3. Если вам нужно иметь дело с несколькими типами исключений, то имейте несколько блоков except в правильном порядке
  4. пользовательские исключения могут быть очень полезны, если в экземплярах исключений необходимо хранить сложную или специфическую информацию
  5. не создавайте новые классы исключений, когда встроенные имеют всю необходимую функциональность
  6. вам не нужно вызывать исключение сразу после его создания, это позволяет легко определить набор допустимых ошибок в одном месте

Вот и все, ребята.