Обработка ошибок увеличивает отказоустойчивость кода, защищая его от потенциальных сбоев, которые могут привести к преждевременному завершению работы.
Прежде чем переходить к обсуждению того, почему обработка исключений так важна, и рассматривать встроенные в Python исключения, важно понять, что есть тонкая грань между понятиями ошибки и исключения.
Ошибку нельзя обработать, а исключения Python обрабатываются при выполнении программы. Ошибка может быть синтаксической, но существует и много видов исключений, которые возникают при выполнении и не останавливают программу сразу же. Ошибка может указывать на критические проблемы, которые приложение и не должно перехватывать, а исключения — состояния, которые стоит попробовать перехватить. Ошибки — вид непроверяемых и невозвратимых ошибок, таких как OutOfMemoryError
, которые не стоит пытаться обработать.
Обработка исключений делает код более отказоустойчивым и помогает предотвращать потенциальные проблемы, которые могут привести к преждевременной остановке выполнения. Представьте код, который готов к развертыванию, но все равно прекращает работу из-за исключения. Клиент такой не примет, поэтому стоит заранее обработать конкретные исключения, чтобы избежать неразберихи.
Ошибки могут быть разных видов:
- Синтаксические
- Недостаточно памяти
- Ошибки рекурсии
- Исключения
Разберем их по очереди.
Синтаксические ошибки (SyntaxError)
Синтаксические ошибки часто называют ошибками разбора. Они возникают, когда интерпретатор обнаруживает синтаксическую проблему в коде.
Рассмотрим на примере.
a = 8
b = 10
c = a b
File "", line 3
c = a b
^
SyntaxError: invalid syntax
Стрелка вверху указывает на место, где интерпретатор получил ошибку при попытке исполнения. Знак перед стрелкой указывает на причину проблемы. Для устранения таких фундаментальных ошибок Python будет делать большую часть работы за программиста, выводя название файла и номер строки, где была обнаружена ошибка.
Недостаточно памяти (OutofMemoryError)
Ошибки памяти чаще всего связаны с оперативной памятью компьютера и относятся к структуре данных под названием “Куча” (heap
). Если есть крупные объекты (или) ссылки на подобные, то с большой долей вероятности возникнет ошибка OutofMemory
. Она может появиться по нескольким причинам:
- Использование 32-битной архитектуры Python (максимальный объем выделенной памяти невысокий, между 2 и 4 ГБ);
- Загрузка файла большого размера;
- Запуск модели машинного обучения/глубокого обучения и много другое;
Обработать ошибку памяти можно с помощью обработки исключений — резервного исключения. Оно используется, когда у интерпретатора заканчивается память и он должен немедленно остановить текущее исполнение. В редких случаях Python вызывает OutofMemoryError
, позволяя скрипту каким-то образом перехватить самого себя, остановить ошибку памяти и восстановиться.
Но поскольку Python использует архитектуру управления памятью из языка C (функция malloc()
), не факт, что все процессы восстановятся — в некоторых случаях MemoryError
приведет к остановке. Следовательно, обрабатывать такие ошибки не рекомендуется, и это не считается хорошей практикой.
Ошибка рекурсии (RecursionError)
Эта ошибка связана со стеком и происходит при вызове функций. Как и предполагает название, ошибка рекурсии возникает, когда внутри друг друга исполняется много методов (один из которых — с бесконечной рекурсией), но это ограничено размером стека.
Все локальные переменные и методы размещаются в стеке. Для каждого вызова метода создается стековый кадр (фрейм), внутрь которого помещаются данные переменной или результат вызова метода. Когда исполнение метода завершается, его элемент удаляется.
Чтобы воспроизвести эту ошибку, определим функцию recursion
, которая будет рекурсивной — вызывать сама себя в бесконечном цикле. В результате появится ошибка StackOverflow
или ошибка рекурсии, потому что стековый кадр будет заполняться данными метода из каждого вызова, но они не будут освобождаться.
def recursion():
return recursion()
recursion()
---------------------------------------------------------------------------
RecursionError Traceback (most recent call last)
in
----> 1 recursion()
in recursion()
1 def recursion():
----> 2 return recursion()
... last 1 frames repeated, from the frame below ...
in recursion()
1 def recursion():
----> 2 return recursion()
RecursionError: maximum recursion depth exceeded
Ошибка отступа (IndentationError)
Эта ошибка похожа по духу на синтаксическую и является ее подвидом. Тем не менее она возникает только в случае проблем с отступами.
Пример:
for i in range(10):
print('Привет Мир!')
File "", line 2
print('Привет Мир!')
^
IndentationError: expected an indented block
Исключения
Даже если синтаксис в инструкции или само выражение верны, они все равно могут вызывать ошибки при исполнении. Исключения Python — это ошибки, обнаруживаемые при исполнении, но не являющиеся критическими. Скоро вы узнаете, как справляться с ними в программах Python. Объект исключения создается при вызове исключения Python. Если скрипт не обрабатывает исключение явно, программа будет остановлена принудительно.
Программы обычно не обрабатывают исключения, что приводит к подобным сообщениям об ошибке:
Ошибка типа (TypeError)
a = 2
b = 'PythonRu'
a + b
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
in
1 a = 2
2 b = 'PythonRu'
----> 3 a + b
TypeError: unsupported operand type(s) for +: 'int' and 'str'
Ошибка деления на ноль (ZeroDivisionError)
10 / 0
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
in
----> 1 10 / 0
ZeroDivisionError: division by zero
Есть разные типы исключений в Python и их тип выводится в сообщении: вверху примеры TypeError
и ZeroDivisionError
. Обе строки в сообщениях об ошибке представляют собой имена встроенных исключений Python.
Оставшаяся часть строки с ошибкой предлагает подробности о причине ошибки на основе ее типа.
Теперь рассмотрим встроенные исключения Python.
Встроенные исключения
BaseException
+-- SystemExit
+-- KeyboardInterrupt
+-- GeneratorExit
+-- Exception
+-- StopIteration
+-- StopAsyncIteration
+-- ArithmeticError
| +-- FloatingPointError
| +-- OverflowError
| +-- ZeroDivisionError
+-- AssertionError
+-- AttributeError
+-- BufferError
+-- EOFError
+-- ImportError
| +-- ModuleNotFoundError
+-- LookupError
| +-- IndexError
| +-- KeyError
+-- MemoryError
+-- NameError
| +-- UnboundLocalError
+-- OSError
| +-- BlockingIOError
| +-- ChildProcessError
| +-- ConnectionError
| | +-- BrokenPipeError
| | +-- ConnectionAbortedError
| | +-- ConnectionRefusedError
| | +-- ConnectionResetError
| +-- FileExistsError
| +-- FileNotFoundError
| +-- InterruptedError
| +-- IsADirectoryError
| +-- NotADirectoryError
| +-- PermissionError
| +-- ProcessLookupError
| +-- TimeoutError
+-- ReferenceError
+-- RuntimeError
| +-- NotImplementedError
| +-- RecursionError
+-- SyntaxError
| +-- IndentationError
| +-- TabError
+-- SystemError
+-- TypeError
+-- ValueError
| +-- UnicodeError
| +-- UnicodeDecodeError
| +-- UnicodeEncodeError
| +-- UnicodeTranslateError
+-- Warning
+-- DeprecationWarning
+-- PendingDeprecationWarning
+-- RuntimeWarning
+-- SyntaxWarning
+-- UserWarning
+-- FutureWarning
+-- ImportWarning
+-- UnicodeWarning
+-- BytesWarning
+-- ResourceWarning
Прежде чем переходить к разбору встроенных исключений быстро вспомним 4 основных компонента обработки исключения, как показано на этой схеме.
Try
: он запускает блок кода, в котором ожидается ошибка.Except
: здесь определяется тип исключения, который ожидается в блокеtry
(встроенный или созданный).Else
: если исключений нет, тогда исполняется этот блок (его можно воспринимать как средство для запуска кода в том случае, если ожидается, что часть кода приведет к исключению).Finally
: вне зависимости от того, будет ли исключение или нет, этот блок кода исполняется всегда.
В следующем разделе руководства больше узнаете об общих типах исключений и научитесь обрабатывать их с помощью инструмента обработки исключения.
Ошибка прерывания с клавиатуры (KeyboardInterrupt)
Исключение KeyboardInterrupt
вызывается при попытке остановить программу с помощью сочетания Ctrl + C
или Ctrl + Z
в командной строке или ядре в Jupyter Notebook. Иногда это происходит неумышленно и подобная обработка поможет избежать подобных ситуаций.
В примере ниже если запустить ячейку и прервать ядро, программа вызовет исключение KeyboardInterrupt
. Теперь обработаем исключение KeyboardInterrupt
.
try:
inp = input()
print('Нажмите Ctrl+C и прервите Kernel:')
except KeyboardInterrupt:
print('Исключение KeyboardInterrupt')
else:
print('Исключений не произошло')
Исключение KeyboardInterrupt
Стандартные ошибки (StandardError)
Рассмотрим некоторые базовые ошибки в программировании.
Арифметические ошибки (ArithmeticError)
- Ошибка деления на ноль (Zero Division);
- Ошибка переполнения (OverFlow);
- Ошибка плавающей точки (Floating Point);
Все перечисленные выше исключения относятся к классу Arithmetic
и вызываются при ошибках в арифметических операциях.
Деление на ноль (ZeroDivisionError)
Когда делитель (второй аргумент операции деления) или знаменатель равны нулю, тогда результатом будет ошибка деления на ноль.
try:
a = 100 / 0
print(a)
except ZeroDivisionError:
print("Исключение ZeroDivisionError." )
else:
print("Успех, нет ошибок!")
Исключение ZeroDivisionError.
Переполнение (OverflowError)
Ошибка переполнение вызывается, когда результат операции выходил за пределы диапазона. Она характерна для целых чисел вне диапазона.
try:
import math
print(math.exp(1000))
except OverflowError:
print("Исключение OverFlow.")
else:
print("Успех, нет ошибок!")
Исключение OverFlow.
Ошибка утверждения (AssertionError)
Когда инструкция утверждения не верна, вызывается ошибка утверждения.
Рассмотрим пример. Предположим, есть две переменные: a
и b
. Их нужно сравнить. Чтобы проверить, равны ли они, необходимо использовать ключевое слово assert
, что приведет к вызову исключения Assertion
в том случае, если выражение будет ложным.
try:
a = 100
b = "PythonRu"
assert a == b
except AssertionError:
print("Исключение AssertionError.")
else:
print("Успех, нет ошибок!")
Исключение AssertionError.
Ошибка атрибута (AttributeError)
При попытке сослаться на несуществующий атрибут программа вернет ошибку атрибута. В следующем примере можно увидеть, что у объекта класса Attributes
нет атрибута с именем attribute
.
class Attributes(obj):
a = 2
print(a)
try:
obj = Attributes()
print(obj.attribute)
except AttributeError:
print("Исключение AttributeError.")
2
Исключение AttributeError.
Ошибка импорта (ModuleNotFoundError)
Ошибка импорта вызывается при попытке импортировать несуществующий (или неспособный загрузиться) модуль в стандартном пути или даже при допущенной ошибке в имени.
import nibabel
---------------------------------------------------------------------------
ModuleNotFoundError Traceback (most recent call last)
in
----> 1 import nibabel
ModuleNotFoundError: No module named 'nibabel'
Ошибка поиска (LookupError)
LockupError
выступает базовым классом для исключений, которые происходят, когда key
или index
используются для связывания или последовательность списка/словаря неверна или не существует.
Здесь есть два вида исключений:
- Ошибка индекса (
IndexError
); - Ошибка ключа (
KeyError
);
Ошибка ключа
Если ключа, к которому нужно получить доступ, не оказывается в словаре, вызывается исключение KeyError
.
try:
a = {1:'a', 2:'b', 3:'c'}
print(a[4])
except LookupError:
print("Исключение KeyError.")
else:
print("Успех, нет ошибок!")
Исключение KeyError.
Ошибка индекса
Если пытаться получить доступ к индексу (последовательности) списка, которого не существует в этом списке или находится вне его диапазона, будет вызвана ошибка индекса (IndexError: list index out of range python).
try:
a = ['a', 'b', 'c']
print(a[4])
except LookupError:
print("Исключение IndexError, индекс списка вне диапазона.")
else:
print("Успех, нет ошибок!")
Исключение IndexError, индекс списка вне диапазона.
Ошибка памяти (MemoryError)
Как уже упоминалось, ошибка памяти вызывается, когда операции не хватает памяти для выполнения.
Ошибка имени (NameError)
Ошибка имени возникает, когда локальное или глобальное имя не находится.
В следующем примере переменная ans
не определена. Результатом будет ошибка NameError
.
try:
print(ans)
except NameError:
print("NameError: переменная 'ans' не определена")
else:
print("Успех, нет ошибок!")
NameError: переменная 'ans' не определена
Ошибка выполнения (Runtime Error)
Ошибка «NotImplementedError»
Ошибка выполнения служит базовым классом для ошибки NotImplemented
. Абстрактные методы определенного пользователем класса вызывают это исключение, когда производные методы перезаписывают оригинальный.
class BaseClass(object):
"""Опередляем класс"""
def __init__(self):
super(BaseClass, self).__init__()
def do_something(self):
# функция ничего не делает
raise NotImplementedError(self.__class__.__name__ + '.do_something')
class SubClass(BaseClass):
"""Реализует функцию"""
def do_something(self):
# действительно что-то делает
print(self.__class__.__name__ + ' что-то делает!')
SubClass().do_something()
BaseClass().do_something()
SubClass что-то делает!
---------------------------------------------------------------------------
NotImplementedError Traceback (most recent call last)
in
14
15 SubClass().do_something()
---> 16 BaseClass().do_something()
in do_something(self)
5 def do_something(self):
6 # функция ничего не делает
----> 7 raise NotImplementedError(self.__class__.__name__ + '.do_something')
8
9 class SubClass(BaseClass):
NotImplementedError: BaseClass.do_something
Ошибка типа (TypeError)
Ошибка типа вызывается при попытке объединить два несовместимых операнда или объекта.
В примере ниже целое число пытаются добавить к строке, что приводит к ошибке типа.
try:
a = 5
b = "PythonRu"
c = a + b
except TypeError:
print('Исключение TypeError')
else:
print('Успех, нет ошибок!')
Исключение TypeError
Ошибка значения (ValueError)
Ошибка значения вызывается, когда встроенная операция или функция получают аргумент с корректным типом, но недопустимым значением.
В этом примере встроенная операция float
получат аргумент, представляющий собой последовательность символов (значение), что является недопустимым значением для типа: число с плавающей точкой.
try:
print(float('PythonRu'))
except ValueError:
print('ValueError: не удалось преобразовать строку в float: 'PythonRu'')
else:
print('Успех, нет ошибок!')
ValueError: не удалось преобразовать строку в float: 'PythonRu'
Пользовательские исключения в Python
В Python есть много встроенных исключений для использования в программе. Но иногда нужно создавать собственные со своими сообщениями для конкретных целей.
Это можно сделать, создав новый класс, который будет наследовать из класса Exception
в Python.
class UnAcceptedValueError(Exception):
def __init__(self, data):
self.data = data
def __str__(self):
return repr(self.data)
Total_Marks = int(input("Введите общее количество баллов: "))
try:
Num_of_Sections = int(input("Введите количество разделов: "))
if(Num_of_Sections < 1):
raise UnAcceptedValueError("Количество секций не может быть меньше 1")
except UnAcceptedValueError as e:
print("Полученная ошибка:", e.data)
Введите общее количество баллов: 10
Введите количество разделов: 0
Полученная ошибка: Количество секций не может быть меньше 1
В предыдущем примере если ввести что-либо меньше 1, будет вызвано исключение. Многие стандартные исключения имеют собственные исключения, которые вызываются при возникновении проблем в работе их функций.
Недостатки обработки исключений в Python
У использования исключений есть свои побочные эффекты, как, например, то, что программы с блоками try-except работают медленнее, а количество кода возрастает.
Дальше пример, где модуль Python timeit
используется для проверки времени исполнения 2 разных инструкций. В stmt1
для обработки ZeroDivisionError
используется try-except, а в stmt2
— if
. Затем они выполняются 10000 раз с переменной a=0
. Суть в том, чтобы показать разницу во времени исполнения инструкций. Так, stmt1
с обработкой исключений занимает больше времени чем stmt2
, который просто проверяет значение и не делает ничего, если условие не выполнено.
Поэтому стоит ограничить использование обработки исключений в Python и применять его в редких случаях. Например, когда вы не уверены, что будет вводом: целое или число с плавающей точкой, или не уверены, существует ли файл, который нужно открыть.
import timeit
setup="a=0"
stmt1 = '''
try:
b=10/a
except ZeroDivisionError:
pass'''
stmt2 = '''
if a!=0:
b=10/a'''
print("time=",timeit.timeit(stmt1,setup,number=10000))
print("time=",timeit.timeit(stmt2,setup,number=10000))
time= 0.003897680000136461
time= 0.0002797570000439009
Выводы!
Как вы могли увидеть, обработка исключений помогает прервать типичный поток программы с помощью специального механизма, который делает код более отказоустойчивым.
Обработка исключений — один из основных факторов, который делает код готовым к развертыванию. Это простая концепция, построенная всего на 4 блоках: try
выискивает исключения, а except
их обрабатывает.
Очень важно поупражняться в их использовании, чтобы сделать свой код более отказоустойчивым.
As it is known that 1.2 – 1.0 = 0.2 . But when you try to the same in python you will surprised by results:
>>> 1.2 - 1.0
Output:
0.199999999999999996
This can be considered as a bug in Python, but it is not. This has little to do with Python, and much more to do with how the underlying platform handles floating-point numbers. It’s a normal case encountered when handling floating-point numbers internally in a system. It’s a problem caused when the internal representation of floating-point numbers, which uses a fixed number of binary digits to represent a decimal number. It is difficult to represent some decimal number in binary, so in many cases, it leads to small roundoff errors. We know similar cases in decimal math, there are many results that can’t be represented with a fixed number of decimal digits, Example
10 / 3 = 3.33333333.......
In this case, taking 1.2 as an example, the representation of 0.2 in binary is 0.00110011001100110011001100…… and so on. It is difficult to store this infinite decimal number internally. Normally a float object’s value is stored in binary floating-point with a fixed precision (typically 53 bits). So we represent 1.2 internally as,
1.0011001100110011001100110011001100110011001100110011
Which is exactly equal to :
1.1999999999999999555910790149937383830547332763671875
Still, you thinking why python is not solving this issue, actually it has nothing to do with python. It happens because it is the way the underlying c platform handles floating-point numbers and ultimately with the inaccuracy, we’ll always have been writing down numbers as a string of fixed number of digits. Note that this is in the very nature of binary floating-point: this is not a bug either in Python or C, and it is not a bug in your code either. You’ll see the same kind of behaviors in all languages that support our hardware’s floating-point arithmetic although some languages may not display the difference by default, or in all output modes). We have to consider this behavior when we do care about math problems with needs exact precisions or using it inside conditional statements. Check floating point section in python documentation for more such behaviours.
Python documentation says that FloatingPointError is raised when a float calculation fails. But what is exactly meant here by «a float calculation»?
I tried adding, multiplying and dividing with floats but never managed to raise this specific error. Instead, i got a TypeError
:
10/'a'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for /: 'int' and 'str'
Can someone help me understand when a FloatingPointError
is raised in python?
Ma0
14.9k3 gold badges33 silver badges64 bronze badges
asked Dec 19, 2016 at 13:39
6
It is part of the fpectl
module. The FloatingPointError
shouldn’t be raised if you don’t explicitly turn it on (fpectl.turnon_sigfpe()
).
However mind the note:
The
fpectl
module is not built by default, and its usage is discouraged and may be dangerous except in the hands of experts. See also the sectionfpectl-limitations
on limitations for more details.
Update: The fpectl
module has been removed as of Python 3.7.
Even with FloatingPointErrors turned on, 10/'a'
will never raise one. It will always raise a TypeError. A FloatingPointError will only be raised for operations that reach the point of actually performing floating-point math, like 1.0/0.0
. 10/'a'
doesn’t get that far.
Ben
7847 silver badges17 bronze badges
answered Dec 19, 2016 at 13:43
MSeifertMSeifert
141k35 gold badges328 silver badges345 bronze badges
4
You can also trigger a FloatingPointError
within numpy
, by setting the appropriate numpy.seterr
(or numpy.errstate
context manager) flag. For an example taken from the documentation:
>>> np.sqrt(-1) nan >>> with np.errstate(invalid='raise'): ... np.sqrt(-1) Traceback (most recent call last): File "<stdin>", line 2, in <module> FloatingPointError: invalid value encountered in sqrt
Interestingly, it also raises FloatingPointError
when all operands are integers:
>>> old_settings = np.seterr(all='warn', over='raise') >>> np.int16(32000) * np.int16(3) Traceback (most recent call last): File "<stdin>", line 1, in <module> FloatingPointError: overflow encountered in short_scalars
The documentation notes the conditions under which the FloatingPointError
will be raised:
The floating-point exceptions are defined in the IEEE 754 standard [1]:
- Division by zero: infinite result obtained from finite numbers.
- Overflow: result too large to be expressed.
- Underflow: result so close to zero that some precision was lost.
- Invalid operation: result is not an expressible number, typically indicates that a NaN was produced.
answered Jun 15, 2018 at 10:46
gerritgerrit
22.7k17 gold badges90 silver badges166 bronze badges
Today we get started with our in-depth Python Exception Handling series by looking at the FloatingPointError. As with most programming languages, the FloatingPointError
in Python indicates that something has gone wrong with a floating point calculation. However, unlike most other languages, Python will not raise a FloatingPointError
by default. The ability to do so must be implemented by including the fpectl
module when building your local Python environment.
In this article we’ll explore the FloatingPointError
by first looking at where it resides in the overall Python Exception Class Hierarchy. We’ll also go over how the fpectl
module can be enabled, and how doing so can allow the raising of FloatingPointErrors
in your own code. Let’s get to it!
The Technical Rundown
All Python exceptions inherit from the BaseException
class, or extend from an inherited class therein. The full exception hierarchy of this error is:
-
BaseException
Exception
-
ArithmeticError
FloatingPointError
Full Code Sample
Below is the full code sample we’ll be using in this article. It can be copied and pasted if you’d like to play with the code yourself and see how everything works.
import fpectl
from gw_utility.logging import Logging
def main():
Logging.line_separator("FLOATING POINT")
test_floating_point()
Logging.line_separator("DIVISION BY ZERO")
test_division_by_zero()
Logging.line_separator("FLOATING POINT DIVISION BY ZERO", 60)
test_floating_point_division_by_zero()
def test_floating_point():
try:
Logging.log(round(24.601 / 3.5, 4))
except FloatingPointError as exception:
# Output expected FloatingPointErrors.
Logging.log_exception(exception)
except Exception as exception:
# Output expected Exceptions.
Logging.log_exception(exception, False)
def test_division_by_zero():
try:
# Divide by zero.
Logging.log(24 / 0)
except FloatingPointError as exception:
# Output expected FloatingPointErrors.
Logging.log_exception(exception)
except Exception as exception:
# Output expected Exceptions.
Logging.log_exception(exception, False)
def test_floating_point_division_by_zero():
try:
# Divide by floating point zero and round.
Logging.log(round(24.601 / 0.0, 4))
except FloatingPointError as exception:
# Output expected FloatingPointErrors.
Logging.log_exception(exception)
except Exception as exception:
# Output expected Exceptions.
Logging.log_exception(exception, False)
if __name__ == " __main__":
main()
import math
import sys
import traceback
class Logging:
separator_character_default = '-'
separator_length_default = 40
@classmethod
def __output(cls, *args, sep: str=' ', end: str='n', file=None):
"""Prints the passed value(s) to the console.
:param args: Values to output.
:param sep: String inserted between values, default a space.
:param end: String appended after the last value, default a newline.
:param file: A file-like object (stream); defaults to the current sys.stdout.
:return: None
"""
print(*args, sep=sep, end=end, file=file)
@classmethod
def line_separator(cls, value: str, length: int=separator_length_default, char: str=separator_character_default):
"""Print a line separator with inserted text centered in the middle.
:param value: Inserted text to be centered.
:param length: Total separator length.
:param char: Separator character.
"""
output = value
if len(value) < length:
# Update length based on insert length, less a space for margin.
length -= len(value) + 2
# Halve the length and floor left side.
left = math.floor(length / 2)
right = left
# If odd number, add dropped remainder to right side.
if length % 2 != 0:
right += 1
# Surround insert with separators.
output = f'{char * left} {value} {char * right}'
cls.__output(output)
@classmethod
def log(cls, *args, sep: str=' ', end: str='n', file=None):
"""Prints the passed value(s) to the console.
:param args: Values to output.
:param sep: String inserted between values, default a space.
:param end: String appended after the last value, default a newline.
:param file: A file-like object (stream); defaults to the current sys.stdout.
"""
cls.__output(*args, sep=sep, end=end, file=file)
@classmethod
def log_exception(cls, exception: BaseException, expected: bool=True):
"""Prints the passed BaseException to the console, including traceback.
:param exception: The BaseException to output.
:param expected: Determines if BaseException was expected.
"""
output = "[{}] {}: {}".format('EXPECTED' if expected else 'UNEXPECTED', type(exception). __name__ , exception)
cls.__output(output)
exc_type, exc_value, exc_traceback = sys.exc_info()
traceback.print_tb(exc_traceback)
Enter fullscreen mode
Exit fullscreen mode
When Should You Use It?
As discussed in the introduction, before a FloatingPointError
can even appear you’ll need to make sure your local Python build includes the fpectl
module. Since this module is not included with most Python builds by default, you’d likely have had to explicitly build your Python with it if desired. Adding the fpectl
module to can be accomplished by using the --with-fpectl
flag when compiling Python. Going through the compilation process of Python is well beyond the scope of this article, but once fpectl
is an included module, you can start testing the FloatingPointError
.
For our example code we’re not doing anything spectacular. In fact, the FloatingPointError
is effectively raised in situations where other ArithmeticErrors
would normally appear, except that you’re using floating point numbers and the fpectl
module is enabled. For example, you might raise a FloatingPointError
where you’d normally get a ZeroDivisionError
by attempting to divide by zero using a floating point value.
We’ve created a few simple testing methods starting with test_floating_point()
:
def test_floating_point():
try:
Logging.log(round(24.601 / 3.5, 4))
except FloatingPointError as exception:
# Output expected FloatingPointErrors.
Logging.log_exception(exception)
except Exception as exception:
# Output expected Exceptions.
Logging.log_exception(exception, False)
Enter fullscreen mode
Exit fullscreen mode
Executing this code works as expected, performing the floating point calculation and rounding the result to four decimal places before outputting the result to our log:
------------ FLOATING POINT ------------
7.0289
Enter fullscreen mode
Exit fullscreen mode
Now, let’s step away from using a floating point value and use regular integers while attempting to divide by zero:
def test_division_by_zero():
try:
# Divide by zero.
Logging.log(24 / 0)
except FloatingPointError as exception:
# Output expected FloatingPointErrors.
Logging.log_exception(exception)
except Exception as exception:
# Output expected Exceptions.
Logging.log_exception(exception, False)
Enter fullscreen mode
Exit fullscreen mode
This raises an unexpected ZeroDivisionException
since, even though fpectl
is enabled, we aren’t using a floating point value in our calculation:
----------- DIVISION BY ZERO -----------
[UNEXPECTED] ZeroDivisionError: division by zero
File "D:/work/Airbrake.io/Exceptions/Python/BaseException/Exception/ArithmeticError/FloatingPointError/main.py", line 30, in test_division_by_zero
Logging.log(24 / 0)
Enter fullscreen mode
Exit fullscreen mode
Finally, let’s try the same division by zero while using floating point values:
def test_floating_point_division_by_zero():
try:
# Divide by floating point zero and round.
Logging.log(round(24.601 / 0.0, 4))
except FloatingPointError as exception:
# Output expected FloatingPointErrors.
Logging.log_exception(exception)
except Exception as exception:
# Output expected Exceptions.
Logging.log_exception(exception, False)
Enter fullscreen mode
Exit fullscreen mode
As you might suspect, this raises a FloatingPointError
for us:
------------- FLOATING POINT DIVISION BY ZERO --------------
[EXPECTED] FloatingPointError: invalid value encountered in divide
File "D:/work/Airbrake.io/Exceptions/Python/BaseException/Exception/ArithmeticError/FloatingPointError/main.py", line 42, in test_floating_point_division_by_zero
Logging.log(round(24.601 / 0.0, 4))
Enter fullscreen mode
Exit fullscreen mode
There we have the basics of using FloatingPointErrors
. However, before you jump into adding the fpectl
module to your Python to distinguish between FloatingPointErrors
and normal ArithmeticErrors
, there are a number of caveats and cautions to be aware of. The IEEE 754
standard for floating point arithmetic defines a number of universal standards for the formatting, rounding, allowed operations, and exception handling practices of floating point numbers. However, your code must be explicitly told to capture IEEE 754
exceptions in the form of SIGFPE
signals generated by the local processor. Consequently, while Python is configured to do so via the fpectl
module, many other custom scripts/applications are not.
The other major consideration is that use of the fpectl
module is generally discouraged, in large part because it is not thread safe. Thread safe applications (that is, most properly developed Python applications) allow data structures to be safely shared between multiple threads without fear of one thread manipulating or altering some data that another thread is using (or where another thread sees different data). However, using the fpectl
module means your floating point data is no longer thread safe, which could cause major issues in multithreaded applications. To be on the safe side, it’s generally recommended that you avoid fpectl
and use another form of application logic to check for arithmetic errors.
Airbrake’s robust error monitoring software provides real-time error monitoring and automatic exception reporting for all your development projects. Airbrake’s state of the art web dashboard ensures you receive round-the-clock status updates on your application’s health and error rates. No matter what you’re working on, Airbrake easily integrates with all the most popular languages and frameworks. Plus, Airbrake makes it easy to customize exception parameters, while giving you complete control of the active error filter system, so you only gather the errors that matter most.
Check out Airbrake’s error monitoring software today and see for yourself why so many of the world’s best engineering teams use Airbrake to revolutionize their exception handling practices!
This article contains affiliate links. See my affiliate disclosure for more information.
Floating-point numbers are a fast and efficient way to store and work with numbers, but they come with a range of pitfalls that have surely stumped many fledgling programmers — perhaps some experienced programmers, too! The classic example demonstrating the pitfalls of floats goes like this:
>>> 0.1 + 0.2 == 0.3
False
Seeing this for the first time can be disorienting. But don’t throw your computer in the trash bin. This behavior is correct!
This article will show you why floating-point errors like the one above are common, why they make sense, and what you can do to deal with them in Python.
Your Computer is a Liar… Sort Of
You’ve seen that 0.1 + 0.2
is not equal to 0.3
but the madness doesn’t stop there. Here are some more confounding examples:
>>> 0.2 + 0.2 + 0.2 == 0.6
False
>>> 1.3 + 2.0 == 3.3
False
>>> 1.2 + 2.4 + 3.6 == 7.2
False
The issue isn’t restricted to equality comparisons, either:
>>> 0.1 + 0.2 <= 0.3
False
>>> 10.4 + 20.8 > 31.2
True
>>> 0.8 - 0.1 > 0.7
True
So what’s going on? Is your computer lying to you? It sure looks like it, but there’s more going on beneath the surface.
When you type the number 0.1
into the Python interpreter, it gets stored in memory as a floating-point number. There’s a conversion that takes place when this happens. 0.1
is a decimal in base 10, but floating-point numbers are stored in binary. In other words, 0.1
gets converted from base 10 to base 2.
The resulting binary number may not accurately represent the original base 10 number. 0.1
is one example. The binary representation is (0.0overline{0011}). That is, 0.1
is an infinitely repeating decimal when written in base 2. The same thing happens when you write the fraction ⅓ as a decimal in base 10. You end up with the infinitely repeating decimal (0.overline{33}).
Computer memory is finite, so the infinitely repeating binary fraction representation of 0.1
gets rounded to a finite fraction. The value of this number depends on your computer’s architecture (32-bit vs. 64-bit). One way to see the floating-point value that gets stored for 0.1
is to use the .as_integer_ratio()
method for floats to get the numerator and denominator of the floating-point representation:
>>> numerator, denominator = (0.1).as_integer_ratio()
>>> f"0.1 ≈ {numerator} / {denominator}"
'0.1 ≈ 3602879701896397 / 36028797018963968'
Now use format()
to show the fraction accurate to 55 decimal places:
>>> format(numerator / denominator, ".55f")
'0.1000000000000000055511151231257827021181583404541015625'
So 0.1
gets rounded to a number slightly larger than its true value.
This error, known as floating-point representation error, happens way more often than you might realize.
Representation Error is Really Common
There are three reasons that a number gets rounded when represented as a floating-point number:
- The number has more significant digits than floating points allow.
- The number is irrational.
- The number is rational but has a non-terminating binary representation.
64-bit floating-point numbers are good for about 16 or 17 significant digits. Any number with more significant digits gets rounded. Irrational numbers, like π and e, can’t be represented by any terminating fraction in any integer base. So again, no matter what, irrational numbers will get rounded when stored as floats.
These two situations create an infinite set of numbers that can’t be exactly represented as a floating-point number. But unless you’re a chemist dealing with tiny numbers, or a physicist dealing with astronomically large numbers, you’re unlikely to run into these problems.
What about non-terminating rational numbers, like 0.1
in base 2? This is where you’ll encounter most of your floating-point woes, and thanks to the math that determines whether or not a fraction terminates, you’ll brush up against representation error more often than you think.
In base 10, a fraction terminates if its denominator is a product of powers of prime factors of 10. The two prime factors of 10 are 2 and 5, so fractions like ½, ¼, ⅕, ⅛, and ⅒ all terminate, but ⅓, ⅐, and ⅑ do not. In base 2, however, there is only one prime factor: 2. So only fractions whose denominator is a power of 2 terminate. As a result, fractions like ⅓, ⅕, ⅙, ⅐, ⅑, and ⅒ are all non-terminating when expressed in binary.
You can now understand the original example in this article. 0.1
, 0.2
, and 0.3
all get rounded when converted to floating-point numbers:
>>> # -----------vvvv Display with 17 significant digits
>>> format(0.1, ".17g")
'0.10000000000000001'
>>> format(0.2, ".17g")
'0.20000000000000001'
>>> format(0.3, ".17g")
'0.29999999999999999'
When 0.1
and 0.2
are added, the result is a number slightly larger than 0.3
:
>>> 0.1 + 0.2
0.30000000000000004
Since 0.1 + 0.2
is slightly larger than0.3
and 0.3
gets represented by a number slightly smaller than itself, the expression 0.1 + 0.2 == 0.3
evaluates to False
.
❗
Floating-point representation error is something every programmer in every language needs to be aware of and know how to handle. It’s not specific to Python. You can see the result of printing 0.1 + 0.2
in many different languages over at Erik Wiffin’s aptly named website 0.30000000000000004.com.
So, how do you deal with floating-point representation errors when comparing floats in Python? The trick is to avoid checking for equality. Never use ==
, >=
, or <=
with floats. Use the math.isclose()
function instead:
>>> import math
>>> math.isclose(0.1 + 0.2, 0.3)
True
math.isclose()
checks if the first argument is acceptably close to the second argument. But what exactly does that mean? The key idea is to examine the distance between the first argument and the second argument, which is equivalent to the absolute value of the difference of the values:
>>> a = 0.1 + 0.2
>>> b = 0.3
>>> abs(a - b)
5.551115123125783e-17
If abs(a - b)
is smaller than some percentage of the larger of a
or b
, then a
is considered sufficiently close to b
to be «equal» to b
. This percentage is called the relative tolerance. You can specify the relative tolerance with the rel_tol
keyword argument of math.isclose()
which defaults to 1e-9
. In other words, if abs(a - b)
is less than 1e-9 * max(abs(a), abs(b))
, then a
and b
are considered «close» to each other. This guarantees that a
and b
are equal to about nine decimal places.
You can change the relative tolerance if you need to:
>>> math.isclose(0.1 + 0.2, 0.3, rel_tol=1e-20)
False
Of course, the relative tolerance depends on constraints set by the problem you’re solving. For most everyday applications, however, the default relative tolerance should suffice.
There’s a problem if one of a
or b
is zero and rel_tol
is less than one, however. In that case, no matter how close the nonzero value is to zero, the relative tolerance guarantees that the check for closeness will always fail. In this case, using an absolute tolerance works as a fallback:
>>> # Relative check fails!
>>> # ---------------vvvv Relative tolerance
>>> # ----------------------vvvvv max(0, 1e-10)
>>> abs(0 - 1e-10) < 1e-9 * 1e-10
False
>>> # Absolute check works!
>>> # ---------------vvvv Absolute tolerance
>>> abs(0 - 1e-10) < 1e-9
True
math.isclose()
will do this check for you automatically. The abs_tol
keyword argument determines the absolute tolerance. However, abs_tol
defaults to 0.0
So you’ll need to set this manually if you need to check how close a value is to zero.
All in all, math.isclose()
returns the result of the following comparison, which combines the relative and absolute tests into a single expression:
abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)
math.isclose()
was introduced in PEP 485 and has been available since Python 3.5.
When Should You Use math.isclose()
?
In general, you should use math.isclose()
whenever you need to compare floating-point values. Replace ==
with math.isclose()
:
>>> # Don't do this:
>>> 0.1 + 0.2 == 0.3
False
>>> # Do this instead:
>>> math.isclose(0.1 + 0.2, 0.3)
True
You also need to be careful with >=
and <=
comparisons. Handle the equality separately using math.isclose()
and then check the strict comparison:
>>> a, b, c = 0.1, 0.2, 0.3
>>> # Don't do this:
>>> a + b <= c
False
>>> # Do this instead:
>>> math.isclose(a + b, c) or (a + b < c)
True
Various alternatives to math.isclose()
exist. If you use NumPy, you can leverage numpy.allclose()
and numpy.isclose()
:
>>> import numpy as np
>>> # Use numpy.allclose() to check if two arrays are equal
>>> # to each other within a tolerance.
>>> np.allclose([1e10, 1e-7], [1.00001e10, 1e-8])
False
>>> np.allclose([1e10, 1e-8], [1.00001e10, 1e-9])
True
>>> # Use numpy.isclose() to check if the elements of two arrays
>>> # are equal to each other within a tolerance
>>> np.isclose([1e10, 1e-7], [1.00001e10, 1e-8])
array([ True, False])
>>> np.isclose([1e10, 1e-8], [1.00001e10, 1e-9])
array([ True, True])
Keep in mind that the default relative and absolute tolerances are not the same as math.isclose()
. The default relative tolerance for both numpy.allclose()
and numpy.isclose()
is 1e-05
and the default absolute tolerance for both is 1e-08
.
math.isclose()
is especially useful for unit tests, although there are some alternatives. Python’s built-in unittest
module has a unittest.TestCase.assertAlmostEqual()
method. However, that method only uses an absolute difference test. It’s also an assertion, meaning that failures raise an AssertionError
, making it unsuitable for comparisons in your business logic.
A great alternative to math.isclose()
for unit testing is the pytest.approx()
function from the pytest
package. Unlike math.isclose()
, pytest.approx()
only takes one argument — namely, the value you expect:
>>> import pytest
>>> 0.1 + 0.2 == pytest.approx(0.3)
True
pytest.approx()
has rel_tol
and abs_tol
keyword arguments for setting the relative and absolute tolerances. The default values are different from math.isclose()
, however. rel_tol
has a default value of 1e-6
and abs_tol
has a default value of 1e-12
.
If the argument passed to pytest.approx()
is array-like, meaning it’s a Python iterable like a list or a tuple, or even a NumPy array, then pytest.approx()
behaves similar to numpy.allclose()
and returns whether or not the two arrays are equal within the tolerances:
>>> import numpy as np
>>> np.array([0.1, 0.2]) + np.array([0.2, 0.4]) == pytest.approx(np.array([0.3, 0.6]))
True
pytest.approx()
will even work with dictionary values:
>>> {'a': 0.1 + 0.2, 'b': 0.2 + 0.4} == pytest.approx({'a': 0.3, 'b': 0.6})
True
Floating-point numbers are great for working with numbers whenever absolute precision isn’t needed. They are fast and memory efficient. But if you do need precision, then there are some alternatives to floats that you should consider.
Floating-Point Alternatives That Are Precise
Two built-in numeric types in Python offer full precision for situations where floats are inadequate: Decimal
and Fraction
.
The Decimal
Type
The Decimal
type can store decimal values exactly with as much precision as you need. By default, Decimal
preserves 28 significant figures, but you can change this to whatever you need to suit the specific problem you’re solving:
>>> # Import the Decimal type from the decimal module
>>> from decimal import Decimal
>>> # Values are represented exactly so no rounding error occurs
>>> Decimal("0.1") + Decimal("0.2") == Decimal("0.3")
True
>>> # By default 28 significant figures are preserved
>>> Decimal(1) / Decimal(7)
Decimal('0.1428571428571428571428571429')
>>> # You can change the significant figures if needed
>>> from decimal import getcontext
>>> getcontext().prec = 6 # Use 6 significant figures
>>> Decimal(1) / Decimal(7)
Decimal('0.142857')
You can read more about the Decimal
type in the Python docs.
The Fraction
Type
Another alternative to floating-point numbers is the Fraction
type. Fraction
can store rational numbers exactly and overcomes representation error issues encountered by floating-point numbers:
>>> # import the Fraction type from the fractions module
>>> from fractions import Fraction
>>> # Instantiate a Fraction with a numerator and denominator
>>> Fraction(1, 10)
Fraction(1, 10)
>>> # Values are represented exactly so no rounding error occurs
>>> Fraction(1, 10) + Fraction(2, 10) == Fraction(3, 10)
True
Both Fraction
and Decimal
offer numerous benefits over standard floating-point values. However, these benefits come at a price: reduced speed and higher memory consumption. If you don’t need absolute precision, you’re better off sticking with floats. But for things like financial and mission-critical applications, the tradeoffs incurred by Fraction
and Decimal
may be worthwhile.
Conclusion
Floating-point values are both a blessing and a curse. They offer fast arithmetic operations and efficient memory use at the cost of inaccurate representation. In this article, you learned:
- Why floating-point numbers are imprecise
- Why floating-point representation error is common
- How to correctly compare floating-point values in Python
- How to represent numbers precisely using Python’s
Fraction
andDecimal
types
If you learned something new, then there might be even more that you don’t know about numbers in Python. For example, did you know the int
type isn’t the only integer type in Python? Find out what the other integer type is and other little-known facts about numbers in my article 3 Things You Might Not Know About Numbers in Python.
3 Things You Might Not Know About Numbers in Python
If you’ve written anything in Python, you’ve probably used a number in one of your programs. But there’s more to numbers than just their raw values.
David Amos
Additional Resources
- Floating-Point Arithmetic: Issues and Limitations
- The Floating-Point Guide
- The Perils of Floating Point
- Floating-Point Math
- What Every Computer Scientist Should Know About Floating-Point Arithmetic
- How to Round Numbers in Python
Thanks to Brian Okken for helping catch an issue with one of the pytest.approx()
examples.
Want more like this?
One email, every Saturday, with one actionable tip.
Always less than 5 minutes of your time.
28.12.2019Python, Дизайн компилятора
Как известно, 1.2 - 1.0 = 0.2
. Но когда вы попробуете то же самое в python, вы будете удивлены результатами:
>>> 1.2 - 1.0
Выход:
0.199999999999999996
Это может рассматриваться как ошибка в Python, но это не так. Это имеет мало общего с Python, и гораздо больше связано с тем, как базовая платформа обрабатывает числа с плавающей запятой. Это нормальный случай, возникающий при обработке чисел с плавающей запятой внутри системы. Это проблема, вызванная внутренним представлением чисел с плавающей точкой, которое использует фиксированное число двоичных цифр для представления десятичного числа. Трудно представить некоторое десятичное число в двоичном виде, поэтому во многих случаях это приводит к небольшим ошибкам округления.
Мы знаем подобные случаи в десятичной математике, есть много результатов, которые не могут быть представлены с фиксированным числом десятичных цифр,
пример
10 / 3 = 3.33333333.......
В этом случае, взяв 1.2 в качестве примера, представление 0.2 в двоичном виде составляет 0.00110011001100110011001100......
и так далее.
Трудно хранить это бесконечное десятичное число внутри страны. Обычно значение объекта с плавающей запятой сохраняется в двоичной форме с плавающей точкой с фиксированной точностью ( обычно 53 бита ).
Таким образом, мы представляем 1,2 внутри, как,
1.0011001100110011001100110011001100110011001100110011
Что в точности равно:
1.1999999999999999555910790149937383830547332763671875
Тем не менее, вы думаете, почему Python не решает эту проблему , на самом деле это не имеет ничего общего с Python. Это происходит потому, что именно так базовая платформа c обрабатывает числа с плавающей запятой и, в конечном счете, с неточностью, мы всегда будем записывать числа в виде строки с фиксированным числом цифр.
Обратите внимание, что это по своей природе двоичная с плавающей точкой: это не ошибка ни в Python, ни в C , и это не ошибка в вашем коде. Вы увидите одинаковое поведение на всех языках, которые поддерживают арифметику с плавающей точкой нашего оборудования, хотя некоторые языки могут не отображать разницу по умолчанию или во всех режимах вывода). Мы должны учитывать это поведение, когда заботимся о математических задачах с точной точностью или используем их в условных выражениях.
Проверьте раздел с плавающей точкой в документации по Python для большего количества таких поведений.
Рекомендуемые посты:
- Регулярное выражение Python | Проверьте, является ли ввод числом с плавающей точкой или нет
- Python программа для преобразования плавающего в двоичный
- Python | Ошибка подтверждения
- Python | Средняя квадратическая ошибка
- Ошибка NZEC в Python
- Программа Python для представления плавающего числа в шестнадцатеричном формате по стандарту IEEE 754
- Python | 404 Обработка ошибок во Flask
- Python | Запрос пароля во время выполнения и завершение с сообщением об ошибке
- Python PIL | Метод Image.point ()
- Python | Самая дальняя точка на горизонтальных линиях в 2D плоскости
- Python | Кроссовер с одной точкой в генетическом алгоритме
- ML | Потеря журнала и средняя квадратическая ошибка
- Обработка ошибок в дизайне компилятора
- Обнаружение ошибок и восстановление в компиляторе
- ML | Математическое объяснение СКО и R-квадрата ошибки
Ошибка с плавающей точкой в Python
0.00 (0%) 0 votes