В вашем терминале Python попробуйте выполнить это
Странно не правда ли? Ответ должен был быть True
. Каждый программист хоть раз сталкивался с этой проблемой. Если нет, то они обязательно столкнутся с этим в будущем. Что касается вопроса «Почему это происходит?», во-первых, мы должны понять, как числа с плавающей запятой представлены внутри компьютера.
На самом деле это не ошибка в Python или любом другом языке. Это вызвано несоответствием между различными системами счисления. Читайте дальше для дальнейшего объяснения.
Во-первых: что такое числа с плавающей запятой?
Числа с плавающей запятой используются для представления дробных чисел (сокращенно дроби) и используются в большинстве инженерных и технических расчетов. Например, 3.256
, 2.1
и 0.0036
являются числами с плавающей запятой. Таким образом, 0.1
может быть выражено в дробной форме как 1/10
и 0.2
как 2/10
.Когда мы возьмем сумму этих двух дробей, мы получим 3/10
, что равно 0.3
.
Довольно просто, не так ли? Это потому, что мы используем base-10
или decimal
систему счисления для повседневных расчетов. Система счисления base-10
использует числа от 0
до 9
для представления и вычислений. Таким образом 0.1 + 0.2 = 0.3
. Когда дело доходит до компьютеров, они понимают языки программирования более высокого уровня, такие как Python, Java, C, преобразуя их и сохраняя в системе счисления base-2
или системе счисления binary
, состоящей из 0s
и 1s
.
Base-10 против Base-2: разборки!
Рассмотрим 1/3
в десятичном формате. Десятичная дробь — это рекурсивная дробь, равная 0.3333333
. Точно так же 2/3
равно 0.66666666
. Взяв сумму двух, мы получим 0.99999999
, а не 1
. Поскольку основанием десятичной системы является 10
, можно точно представить только дроби со знаменателем, содержащим один из простых множителей числа 10
, т.е. 2
и 5
. Точно так же в двоичной системе могут быть точно представлены только дроби со знаменателем, содержащим только простой множитель 2
, который сам является 2
. К сожалению, для большинства десятичных чисел нет точного представления в двоичной системе счисления.
Назад к 0.1 + 0.2
В base-2
1/10
— это бесконечно повторяющаяся дробь 0.0001100110011001100110011001100110011001100110011...
. То же самое и с 2/10
, который равен 0.001100110011001100110011001100110011...
. Остановка на любом конечном числе приводит к приближению. Сегодня на большинстве машин числа с плавающей запятой аппроксимируются с использованием стандарта IEEE с плавающей запятой двойной точности, который использует 64 bits
для представлений.
Первый бит — это бит знака, S, следующие 11 бит — это биты экспоненты, E, а последние 52 бита — это дробь F. Соответственно, ближайшим десятичным значением усеченного значения 0.1
будет 0.1000000000000000055511151231257827021181583404541015625
, что близко к 0.1
, но не совсем равно. Точно так же для 0.2
это 0.200000000000000011102230246251565404236316680908203125
.
Большинство языков программирования возвращают 0.30000000000000004
как сумму 0.1
и 0.2
. Но что интересно, вы можете видеть, что 0.1000000000000000055511151231257827021181583404541015625
+ 0.200000000000000011102230246251565404236316680908203125
не равно 0.30000000000000004
. Это связано с тем, что когда машина добавляет 0.1
и 0.2
, она видит только усеченные двоичные значения 0.1
и 0.2
. И неудивительно, что наилучшим приближением к сумме этих двух двоичных чисел является значение чуть выше 0.3
: примерно 0.30000000000000004
.
Но подождите, это еще не все!
Эти ошибки аппроксимации также появляются, когда вы пытаетесь округлить числа! Вы ожидаете, что округление 2.675
до двух знаков после десятичной точки вернет 2.68
. Но нет! Он возвращает 2.67
, поскольку 2.675
, преобразованное в двоичное число с плавающей запятой, снова заменяется двоичным приближением, точное значение которого равно 2.67499999999999982236431605997495353221893310546875
, что ближе к 2.67
, чем 2.68
.
Решение для Python
При работе с десятичными вычислениями считается хорошей практикой всегда округлять десятичные дроби до количества позиций, которое требуется для программирования. В сценариях, где важно, как округляются десятичные дроби или точность арифметики с плавающей запятой, в Python есть модуль с именем decimal
.
Если вы не занимаетесь проектами, которые определяют траектории полета или целостность атомной электростанции, или обрабатываете финансовую информацию компаний, аппроксимации с плавающей запятой не представляют большой проблемы. Целесообразно помнить об этом, а не забывать об этом, когда вы выполняете вычисления с плавающей запятой на своем языке.
Удачного кодирования!