Python исключение в конструкторе

Обновлено: 25.04.2024

Что такое исключения? Из названия понятно — они возникают, когда в программе происходит исключительная ситуация. Вы спросите, почему исключения — анти-паттерн, и как они вообще относятся к типизации? Я попробовал разобраться, и теперь хочу обсудить это с вами, хабражители.

Проблемы исключений

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

Исключения трудно заметить

Существует два типа исключений: «явные» создаются при помощи вызова raise прямо в коде, который вы читаете; «скрытые» запрятаны в используемых функциях, классах, методах.

Проблема в том, что «скрытые» исключения и правда трудно заметить. Покажу на примере чистой функции:


Функция просто делит одно число на другое, возвращая float . Типы проверены и можно запустить что-то такое:


Заметили? На самом деле до print исполнение программы никогда не дойдет, потому что деление 1 на 0 – невозможная операция, она вызовет ZeroDivisionError . Да, такой код безопасен с точки зрения типов, но его все равно нельзя использовать.

Чтобы заметить потенциальную проблему даже в таком максимально простом и читаемом коде, нужен опыт. Все что угодно в Python может перестать работать с разными типами исключений: деление, вызовы функций, int , str , генераторы, итераторы в циклах, доступ к атрибутам или ключам. Даже сам raise something() может привести к сбою. Причем, я даже не упоминаю операции ввода и вывода. А проверенные исключения перестанут поддерживаться в ближайшем будущем.

Восстановление нормального поведения на месте невозможно

Но именно на такой случай у нас же есть исключения. Давайте просто обработаем ZeroDivisionError , и код станет безопасным с точки зрения типов.


Теперь всё в порядке. Но почему мы возвращаем 0? Почему не 1 или None ? Конечно, в большинстве случаев, получить None почти так же плохо (если даже не хуже), как исключение, но все же нужно опираться на бизнес-логику и варианты использования функции.

Что именно мы делим? Произвольные числа, какие-то конкретные единицы или деньги? Не каждый вариант легко предусмотреть и восстановить. Может получиться, что при последующем использовании одной функции обнаружится, что потребуется другая логика восстановления.

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

Нет серебряной пули, которая бы справилась с ZeroDivisionError раз и навсегда. И это мы ещё не говорим о возможности сложного ввода-вывода с повторными запросами и таймаутами.

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

Процесс выполнения неясен

Хорошо, давайте понадеемся, что кто-то другой поймает исключение и, возможно, справится с ним. Например, система может запросить у пользователя изменить введенное значение, потому что нельзя делить на 0. И функция divide явно не должна отвечать за восстановление после ошибки.

В таком случае нужно проверить, где мы поймали исключение. Кстати, как определить, где именно оно обработается? Возможно ли перейти в нужное место кода? Оказывается, что нет, невозможно.

Невозможно определить, какая строка кода выполнится после создания исключения. Разные типы исключений могут обрабатываться с помощью разных вариантов except , а некоторые исключения могут игнорироваться. А еще вы можете выкинуть дополнительные исключения в других модулях, которые выполнятся раньше, и вообще сломают всю логику.

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

Только с включенным отладчиком в режиме «ловить все исключения».


Исключения, как пресловутое goto , рвут структуру программы.

Исключения не исключительны


В этом примере буквально все может пойти не так. Вот неполный список возможных ошибок:

  • Сеть может быть недоступна, и запрос вообще не будет выполняться.
  • Может не работать сервер.
  • Сервер может быть слишком занят, наступит таймаут.
  • Сервер может потребовать аутентификацию.
  • У API может не быть такого URL.
  • Может быть передан несуществующий пользователь.
  • Может быть недостаточно прав.
  • Сервер может упасть из-за внутренней ошибки при обработке вашего запроса
  • Сервер может вернуть невалидный или поврежденный ответ.
  • Сервер может вернуть невалидный JSON, который не удастся распарсить.

Как себя обезопасить?

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

  • Везде написать except Exception: pass . Тупиковый путь. Не делайте так.
  • Возвращать None . Тоже зло. В итоге либо придется почти каждую строку начинать с if something is not None: и вся логика потеряется за мусором очищающих проверок, либо все время страдать от TypeError . Не самый приятный выбор.
  • Писать классы для особых случаев использования. Например, базовый класс User с подклассами для ошибок типа UserNotFound и MissingUser . Такой подход вполне можно использовать в некоторых конкретных ситуациях, таких как AnonymousUser в Django, но обернуть все возможные ошибки в классы нереально. Потребуется слишком много работы, и доменная модель станет невообразимо сложной.
  • Использовать контейнеры, чтобы обернуть полученное значение переменной или ошибки в обертку и дальше работать уже со значением контейнера. Вот почему мы создали проект @dry-python/return . Чтобы функции возвращали что-то осмысленное, типизированное и безопасное.


Заключим значения в одну из двух оберток: Success или Failure . Данные классы наследуются от базового класса Result . Типы упакованных значений можно указать в аннотации возвращаемой функцией, например, Result[float, ZeroDivisionError] возвращает либо Success[float] , либо Failure[ZeroDivisionError] .

Что это нам дает? Больше исключения не исключительные, а представляют собой ожидаемые проблемы. Также оборачивание исключения в Failure решает вторую проблему: сложность определения потенциальных исключений.


Теперь их легко заметить. Если видите в коде Result , значит функция может выдать исключение. И вы даже заранее знаете его тип.

Более того, библиотека полностью типизирована и совместима с PEP561. То есть mypy предупредит вас, если вы попытаетесь вернуть что-то, что не соответствует объявленному типу.

Как работать с контейнерами?

  • map для функций, которые возвращают обычные значения;
  • bind для функций, которые возвращают другие контейнеры.

Прелесть в том, что такой код защитит вас от неудачных сценариев, поскольку .bind и .map не выполнятся для контейнеров c Failure :


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

Но как развернуть значения из контейнеров?

Действительно, если вы работаете с функциями, которые ничего не знают про контейнеры, вам нужны именно сами значения. Тогда можно использовать методы .unwrap() или .value_or() :


Подождите, мы должны были избавиться от исключений, а теперь выясняется, что все вызовы .unwrap() могут привести к еще одному исключению?

Как не думать об UnwrapFailedErrors?

Хорошо, давайте посмотрим, как жить с новыми исключениями. Рассмотрим такой пример: нужно проверить пользовательский ввод и создать две модели в базе данных. Каждый шаг может завершиться исключением, вот почему все методы обернуты в Result :


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


Все сработает без каких-либо проблем, не вызовутся никакие исключения, потому что не используется .unwrap() . Но легко ли читать такой код? Нет. А какая есть альтернатива? @pipeline :


Теперь данный код отлично читается. Вот как .unwrap() и @pipeline работают вместе: всякий раз, когда какой-либо метод .unwrap() завершается неудачей и Failure[str] , декоратор @pipeline ловит её и возвращает Failure[str] в качестве результирующего значения. Вот так я предлагаю удалить все исключения из кода и сделать его действительно безопасным и типизированным.

Оборачиваем все вместе


Первый, с @safe , проще и лучше читается.

Последнее, что нужно сделать в примере с запросом к API – добавить декоратор @safe . В итоге получится такой код:


Подведем итог, как избавиться от исключений и обезопасить код:

  • Использовать обертку @safe для всех методов, которые могут вызвать исключение. Она изменит тип возвращаемого значения функции на Result[OldReturnType, Exception] .
  • Использовать Result как контейнер, чтобы перенести значения и ошибки в простую абстракцию.
  • Использовать .unwrap() , чтобы развернуть значение из контейнера.
  • Использовать @pipeline , чтобы последовательности вызовов .unwrap легче читались.
  • «Исключения трудно заметить». Теперь они обернуты в типизированный контейнер Result , что делает их совершенно прозрачными.
  • «Восстановление нормального поведения на месте невозможно». Теперь можно смело делегировать процесс восстановления вызывающей стороне. На такой случай есть .fix() и .rescue() .
  • «Последовательность исполнения неясна». Теперь они едины с обычным бизнес-потоком. От начала и до конца.
  • «Исключения не являются исключительными». Мы знаем! И мы ожидаем, что что-то пойдет не так и готовы ко всему.

Варианты использования и ограничения

Очевидно, не получится использовать такой подход во всем вашем коде. Будет слишком безопасно для большинства повседневных ситуаций и несовместимо с другими библиотеками или фреймворками. Но важнейшие части вашей бизнес-логики вы должны писать именно так, как я показал, чтобы обеспечить правильность работы вашей системы и облегчить поддержку в будущем.

Тема заставляет задуматься или даже кажется холиварной? Приходите на Moscow Python Conf++ 5 апреля, обсудим! Кроме меня там будет Артём Малышев – основатель проекта dry-python и core-разработчик Django Channels. Он расскажет еще больше интересного про dry-python и бизнес-логику.

Until now error messages haven’t been more than mentioned, but if you have tried out the examples you have probably seen some. There are (at least) two distinguishable kinds of errors: syntax errors and exceptions.

8.1. Syntax Errors¶

Syntax errors, also known as parsing errors, are perhaps the most common kind of complaint you get while you are still learning Python:

The parser repeats the offending line and displays a little ‘arrow’ pointing at the earliest point in the line where the error was detected. The error is caused by (or at least detected at) the token preceding the arrow: in the example, the error is detected at the function print() , since a colon ( ':' ) is missing before it. File name and line number are printed so you know where to look in case the input came from a script.

8.2. Exceptions¶

The last line of the error message indicates what happened. Exceptions come in different types, and the type is printed as part of the message: the types in the example are ZeroDivisionError , NameError and TypeError . The string printed as the exception type is the name of the built-in exception that occurred. This is true for all built-in exceptions, but need not be true for user-defined exceptions (although it is a useful convention). Standard exception names are built-in identifiers (not reserved keywords).

The rest of the line provides detail based on the type of exception and what caused it.

The preceding part of the error message shows the context where the exception occurred, in the form of a stack traceback. In general it contains a stack traceback listing source lines; however, it will not display lines read from standard input.

Built-in Exceptions lists the built-in exceptions and their meanings.

8.3. Handling Exceptions¶

It is possible to write programs that handle selected exceptions. Look at the following example, which asks the user for input until a valid integer has been entered, but allows the user to interrupt the program (using Control - C or whatever the operating system supports); note that a user-generated interruption is signalled by raising the KeyboardInterrupt exception.

The try statement works as follows.

First, the try clause (the statement(s) between the try and except keywords) is executed.

If no exception occurs, the except clause is skipped and execution of the try statement is finished.

If an exception occurs during execution of the try clause, the rest of the clause is skipped. Then, if its type matches the exception named after the except keyword, the except clause is executed, and then execution continues after the try/except block.

If an exception occurs which does not match the exception named in the except clause, it is passed on to outer try statements; if no handler is found, it is an unhandled exception and execution stops with a message as shown above.

A try statement may have more than one except clause, to specify handlers for different exceptions. At most one handler will be executed. Handlers only handle exceptions that occur in the corresponding try clause, not in other handlers of the same try statement. An except clause may name multiple exceptions as a parenthesized tuple, for example:

A class in an except clause is compatible with an exception if it is the same class or a base class thereof (but not the other way around — an except clause listing a derived class is not compatible with a base class). For example, the following code will print B, C, D in that order:

Note that if the except clauses were reversed (with except B first), it would have printed B, B, B — the first matching except clause is triggered.

All exceptions inherit from BaseException , and so it can be used to serve as a wildcard. Use this with extreme caution, since it is easy to mask a real programming error in this way! It can also be used to print an error message and then re-raise the exception (allowing a caller to handle the exception as well):

Alternatively the last except clause may omit the exception name(s), however the exception value must then be retrieved from sys.exc_info()[1] .

The try … except statement has an optional else clause, which, when present, must follow all except clauses. It is useful for code that must be executed if the try clause does not raise an exception. For example:

The use of the else clause is better than adding additional code to the try clause because it avoids accidentally catching an exception that wasn’t raised by the code being protected by the try … except statement.

When an exception occurs, it may have an associated value, also known as the exception’s argument. The presence and type of the argument depend on the exception type.

The except clause may specify a variable after the exception name. The variable is bound to an exception instance with the arguments stored in instance.args . For convenience, the exception instance defines __str__() so the arguments can be printed directly without having to reference .args . One may also instantiate an exception first before raising it and add any attributes to it as desired.

If an exception has arguments, they are printed as the last part (‘detail’) of the message for unhandled exceptions.

Exception handlers don’t just handle exceptions if they occur immediately in the try clause, but also if they occur inside functions that are called (even indirectly) in the try clause. For example:

8.4. Raising Exceptions¶

The raise statement allows the programmer to force a specified exception to occur. For example:

The sole argument to raise indicates the exception to be raised. This must be either an exception instance or an exception class (a class that derives from Exception ). If an exception class is passed, it will be implicitly instantiated by calling its constructor with no arguments:

If you need to determine whether an exception was raised but don’t intend to handle it, a simpler form of the raise statement allows you to re-raise the exception:

8.5. Exception Chaining¶

The raise statement allows an optional from which enables chaining exceptions. For example:

This can be useful when you are transforming exceptions. For example:

Exception chaining happens automatically when an exception is raised inside an except or finally section. This can be disabled by using from None idiom:

For more information about chaining mechanics, see Built-in Exceptions .

8.6. User-defined Exceptions¶

Programs may name their own exceptions by creating a new exception class (see Classes for more about Python classes). Exceptions should typically be derived from the Exception class, either directly or indirectly.

Exception classes can be defined which do anything any other class can do, but are usually kept simple, often only offering a number of attributes that allow information about the error to be extracted by handlers for the exception.

Most exceptions are defined with names that end in “Error”, similar to the naming of the standard exceptions.

Many standard modules define their own exceptions to report errors that may occur in functions they define. More information on classes is presented in chapter Classes .

8.7. Defining Clean-up Actions¶

The try statement has another optional clause which is intended to define clean-up actions that must be executed under all circumstances. For example:

If a finally clause is present, the finally clause will execute as the last task before the try statement completes. The finally clause runs whether or not the try statement produces an exception. The following points discuss more complex cases when an exception occurs:

If an exception occurs during execution of the try clause, the exception may be handled by an except clause. If the exception is not handled by an except clause, the exception is re-raised after the finally clause has been executed.

An exception could occur during execution of an except or else clause. Again, the exception is re-raised after the finally clause has been executed.

If the finally clause executes a break , continue or return statement, exceptions are not re-raised.

If the try statement reaches a break , continue or return statement, the finally clause will execute just prior to the break , continue or return statement’s execution.

If a finally clause includes a return statement, the returned value will be the one from the finally clause’s return statement, not the value from the try clause’s return statement.

A more complicated example:

As you can see, the finally clause is executed in any event. The TypeError raised by dividing two strings is not handled by the except clause and therefore re-raised after the finally clause has been executed.

In real world applications, the finally clause is useful for releasing external resources (such as files or network connections), regardless of whether the use of the resource was successful.

8.8. Predefined Clean-up Actions¶

Some objects define standard clean-up actions to be undertaken when the object is no longer needed, regardless of whether or not the operation using the object succeeded or failed. Look at the following example, which tries to open a file and print its contents to the screen.

The problem with this code is that it leaves the file open for an indeterminate amount of time after this part of the code has finished executing. This is not an issue in simple scripts, but can be a problem for larger applications. The with statement allows objects like files to be used in a way that ensures they are always cleaned up promptly and correctly.

After the statement is executed, the file f is always closed, even if a problem was encountered while processing the lines. Objects which, like files, provide predefined clean-up actions will indicate this in their documentation.

В Python пользователи могут определять свои собственные исключения, создавая новый класс. Этот класс исключений должен прямо или косвенно быть производным от встроенного класса Exception . Большинство встроенных исключений также являются производными от этого класса. Все пользовательские исключения также должны быть производными от этого класса.

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

При разработке программы на Python, хорошей практикой считается помещать все определяемые пользователем исключения в отдельный файл. Многие стандартные модули определяют свои исключения отдельно как exceptions.py или errors.py (обычно, но не всегда).

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

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

В примере определен базовый класс под названием Error() . Два других исключения, которые фактически вызываются программой ( ValueTooSmallError и ValueTooLargeError ), являются производными от класса Error() .

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

Пример запуска скрипта с примером:

Настройка собственных классов исключений.

Для тонкой настройки своего класса исключения нужно иметь базовые знания объектно-ориентированного программирования.

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

В примере, для приема аргументов salary и message переопределяется конструктор встроенного класса Exception() . Затем конструктор родительского класса Exception() вызывается вручную с аргументом self.message при помощи функции super() . Пользовательский атрибут self.salary определен для использования позже.

Результаты запуска скрипта:

Вывод работы скрипта:

Как перехватывать пользовательское исключение.

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

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

В примере ловиться исключение, определяемое модулем sqlite3 , а затем вызывается пользовательское исключение при помощи raise .

В Python все исключения должны быть экземплярами класса, производного от BaseException. В конструкции try с инструкцией except , которое упоминает определенный класс, это выражение также обрабатывает любые классы исключений, производные от этого класса, но не классы исключений, из которых он производен. Два класса исключений, которые не связаны через подклассы, никогда не эквивалентны, даже если они имеют одно и то же имя.

Встроенные исключения, могут быть сгенерированы интерпретатором или встроенными функциями. За исключением некоторых случаев, они имеют "ассоциированное значение", указывающее подробную причину ошибки. Это может быть строка или кортеж из нескольких элементов информации, например код ошибки и строка объясняющая код. Связанное значение обычно передается в качестве аргументов конструктору класса исключений.

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

При вызове или повторном вызове исключения в инструкции except или finally - __context__ автоматически устанавливает значение последнего перехваченного исключения. Если новое исключение не обрабатывается, то в конечном итоге отображается обратная трассировка, включающая исходное исключение и окончательное исключение.

При вызове нового исключения вместо использования простого вызова для повторного вызова обрабатываемого в данный момент исключения, контекст неявного исключения можно дополнить явной причиной с помощью оператора raise :

Выражение следующее за from , должно быть исключением или None . Оно будет установлен как __cause__ для вызванного исключения. Установка __cause__ также неявно устанавливает атрибут __suppress_context__ в True , так что использование raise new_exc from None эффективно заменяет старое исключение новым для целей отображения, например преобразование KeyError в AttributeError , оставляя старое исключение доступным в __context__ для самоанализа при отладке.

Код отображения обратной трассировки по умолчанию показывает эти связанные исключения в дополнение к обратной трассировке для самого исключения. Явное цепное исключение в __cause__ , при наличии, всегда отображается. Неявно связанное исключение в __context__ показывается, только если __cause__ установлено в None и __suppress_context__ ложно.

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

Иерархия встроенных исключений в Python.

Иерархия встроенных исключений.

Базовые классы исключений в Python.

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

Исключения, наследуемые от BaseException в Python.

Исключения, наследуемые от BaseException.

Исключения наследуемые от Exception в Python.

Все перечисленные здесь встроенные исключения, являются производными от класса Exception, который в свою очередь наследуется от базового класса BaseException

Арифметические ошибки: ArithmeticError в Python.

Исключение ArithmeticError - базовый класс для тех встроенных исключений: OverflowError, ZeroDivisionError, FloatingPointError

Исключения операционной системы: OSError в Python.

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

Категория исключений: Warning в Python.

Следующие исключения используются в качестве категорий предупреждений.

Ошибка кодировки: UnicodeError в Python.

Исключение UnicodeError поднимается, когда возникает ошибка кодирования или декодирования, связанная с Unicode. Это подкласс ValueError

Создание пользовательского класса исключения в Python.

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

Создание собственных классов ошибок

Естественно, важен и сам тип ошибки, и мы часто создаем собственные типы ошибок, чтобы обозначить конкретную ситуацию, которая обычно не покрывается на уровне языка Python. Таким образом, пользователи класса, встретив такую ошибку, будут в точности знать, что происходит.

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

Собственный класс исключений MyCustomError

При выдаче исключения требуются методы __init__() и __str__() .

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


В вышеприведенном классе MyCustomError есть два волшебных метода, __init__ и __str__ , автоматически вызываемых в процессе обработки исключений. Метод Init вызывается при создании экземпляра, а метод str – при выводе экземпляра на экран. Следовательно, при выдаче исключения два этих метода обычно вызываются сразу друг за другом. Оператор вызова исключения в Python переводит программу в состояние ошибки.

В списке аргументов метода __init__ есть *args . Компонент *args – это особый режим сопоставления с шаблоном, используемый в функциях и методах. Он позволяет передавать множественные аргументы, а переданные аргументы хранит в виде кортежа, но при этом позволяет вообще не передавать аргументов.

В нашем случае можно сказать, что, если конструктору MyCustomError были переданы какие-либо аргументы, то мы берем первый переданный аргумент и присваиваем его атрибуту message в объекте. Если ни одного аргумента передано не было, то атрибуту message будет присвоено значение None .


Исключение MyCustomError выдается без каких-либо аргументов (скобки пусты). Иными словами, такая конструкция объекта выглядит нестандартно. Но это просто синтаксическая поддержка, оказываемая в Python при выдаче исключения.


Код для класса исключения MyCustomError находится здесь.

Класс CustomIntFloatDic

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

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

Создавая собственный словарь, нужно учитывать, что в нем есть два места, где в словарь могут добавляться значения. Во-первых, это может происходить в методе init при создании объекта (на данном этапе объекту уже могут быть присвоены ключи и значения), а во-вторых — при установке ключей и значений прямо в словаре. В обоих этих местах требуется написать код, гарантирующий, что значение может относиться только к типу int или float .

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

Если создан экземпляр класса CustomIntFloatDict , причем, параметрам ключа и значения не передано никаких аргументов, то они будут установлены в None . Выражение if интерпретируется так: если или ключ равен None , или значение равно None , то с объектом будет вызван метод get_dict() , который вернет атрибут empty_dict ; такой атрибут у объекта указывает на пустой список. Помните, что атрибуты класса доступны у всех экземпляров класса.


Назначение этого класса — позволить пользователю передать список или кортеж с ключами и значениями внутри. Если пользователь вводит список или кортеж в поисках ключей и значений, то два эти перебираемых множества будут сцеплены при помощи функции zip языка Python. Подцепленная переменная, указывающая на объект zip , поддается перебору, а кортежи поддаются распаковке. Перебирая кортежи, я проверяю, является ли val экземпляром класса int или float . Если val не относится ни к одному из этих классов, я выдаю собственное исключение IntFloatValueError и передаю ему val в качестве аргумента.

Класс исключений IntFloatValueError

При выдаче исключения IntFloatValueError мы создаем экземпляр класса IntFloatValueError и одновременно выводим его на экран. Это означает, что будут вызваны волшебные методы init и str .


Классы исключений IntFloatValueError и KeyValueConstructError


Если ни одно исключение не выдано, то есть, все val из сцепленного объекта относятся к типам int или float , то они будут установлены при помощи __setitem__() , и за нас все сделает метод из родительского класса dict , как показано ниже.


Класс KeyValueConstructError

Что произойдет, если пользователь введет тип, не являющийся списком или кортежем с ключами и значениями?

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

Если пользователь не укажет ключи и значения как список или кортеж, то будет выдано исключение KeyValueConstructError . Цель этого исключения – проинформировать пользователя, что для записи ключей и значений в объект CustomIntFloatDict , список или кортеж должен быть указан в конструкторе init класса CustomIntFloatDict .

Далее я даже включаю типы данных, присущие объектам, добавленным к конструктору init – делаю это для большей ясности.


Установка ключа и значения в CustomIntFloatDict

CustomIntFloatDict наследует от dict . Это означает, что он будет функционировать в точности как словарь, везде за исключением тех мест, которые мы выберем для точечного изменения его поведения.

__setitem__ — это волшебный метод, вызываемый при установке ключа и значения в словаре. В нашей реализации setitem мы проверяем, чтобы значение относилось к типу int или float , и только после успешной проверки оно может быть установлено в словаре. Если проверка не пройдена, то можно еще раз воспользоваться классом исключения IntFloatValueError . Здесь можно убедиться, что, попытавшись задать строку ‘bad_value’ в качестве значения в словаре test_4 , мы получим исключение.


Весь код к этому руководству показан ниже и выложен на Github.

Заключение

Как бы то ни было, классы исключений значительно упрощают обработку всех возникающих ошибок!

Читайте также: