Странное поведение с поплавками и преобразованием строк
Я набрал это в оболочку python:
>>> 0.1*0.1
0.010000000000000002
Я ожидал, что 0,1 * 0,1 не 0,01, потому что я знаю, что 0,1 в основании 10 является периодическим в основании 2.
>>> len(str(0.1*0.1))
4
Я ожидал получить 20, поскольку я видел 20 символов выше. Почему я получаю 4?
>>> str(0.1*0.1)
'0.01'
Хорошо, это объясняет, почему я len
дает мне 4, но почему str
возвращает '0.01'
?
>>> repr(0.1*0.1)
'0.010000000000000002'
Почему str
round, но repr
нет? (Я прочитал этот ответ, но я хотел бы знать, как они решили, когда str
округляет float, а когда нет)
>>> str(0.01) == str(0.0100000000001)
False
>>> str(0.01) == str(0.01000000000001)
True
Таким образом, это проблема с точностью поплавков. Я думал, что Python будет использовать одиночные префиксы IEEE 754. Поэтому я проверил его следующим образом:
#include <stdint.h>
#include <stdio.h> // printf
union myUnion {
uint32_t i; // unsigned integer 32-bit type (on every machine)
float f; // a type you want to play with
};
int main() {
union myUnion testVar;
testVar.f = 0.01000000000001f;
printf("%f\n", testVar.f);
testVar.f = 0.01000000000000002f;
printf("%f\n", testVar.f);
testVar.f = 0.01f*0.01f;
printf("%f\n", testVar.f);
}
Я получил:
0.010000
0.010000
0.000100
Python дает мне:
>>> 0.01000000000001
0.010000000000009999
>>> 0.01000000000000002
0.010000000000000019
>>> 0.01*0.01
0.0001
Почему Python дает мне эти результаты?
(Я использую Python 2.6.5. Если вы знаете о различиях в версиях Python, я также буду заинтересован в них.)
Ответы
Ответ 1
Важнейшим требованием repr
является то, что он должен находиться в обратном направлении; то есть eval(repr(f)) == f
должен давать True
во всех случаях.
В Python 2.x(до 2.7) repr
работает, делая printf
с форматом %.17g
и отбрасывая конечные нули. Это гарантировано правильно (для 64-битных поплавков) по IEEE-754. Начиная с 2.7 и 3.1, Python использует более интеллектуальный алгоритм, который может найти более короткие представления в некоторых случаях, когда %.17g
дает ненужные ненулевые конечные цифры или терминальные девятки. См. Что нового в 3.1? и проблема 1580.
Даже в Python 2.7, repr(0.1 * 0.1)
дает "0.010000000000000002"
. Это связано с тем, что 0.1 * 0.1 == 0.01
является False
под анализом и арифметикой IEEE-754; то есть ближайшее 64-битовое значение с плавающей запятой до 0.1
, при умножении на себя, дает 64-битовое значение с плавающей запятой, которое не является ближайшим значением с 64-разрядной плавающей запятой до 0.01
:
>>> 0.1.hex()
'0x1.999999999999ap-4'
>>> (0.1 * 0.1).hex()
'0x1.47ae147ae147cp-7'
>>> 0.01.hex()
'0x1.47ae147ae147bp-7'
^ 1 ulp difference
Разница между repr
и str
(pre-2.7/3.1) заключается в том, что str
форматы с 12 знаками после запятой в отличие от 17, которые не являются округлыми, но дают во многих случаях более читаемые результаты.
Ответ 2
Я могу подтвердить ваше поведение
ActivePython 2.6.4.10 (ActiveState Software Inc.) based on
Python 2.6.4 (r264:75706, Jan 22 2010, 17:24:21) [MSC v.1500 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> repr(0.1)
'0.10000000000000001'
>>> repr(0.01)
'0.01'
Теперь docs заявляет, что в Python < 2.7
значение repr(1.1)
было вычислено как format(1.1, '.17g')
Это небольшое упрощение.
Обратите внимание, что это все связано с кодом форматирования строки - в памяти все поплавки Python просто хранятся, поскольку С++ удваивается, поэтому между ними не будет никакой разницы.
Кроме того, неприятно работать с полноразмерной строкой для float, даже если вы знаете, что там лучше. Действительно, в современных Pythons для форматирования float используется новый алгоритм, который выбирает кратчайшее представление интеллектуальным способом.
Я потратил некоторое время, глядя на исходный код, поэтому я расскажу подробности здесь, если вы заинтересованы. Вы можете пропустить этот раздел.
В floatobject.c
, мы видим, что
static PyObject *
float_repr(PyFloatObject *v)
{
char buf[100];
format_float(buf, sizeof(buf), v, PREC_REPR);
return PyString_FromString(buf);
}
что заставляет нас смотреть на format_float
. Опуская специальные случаи NaN/inf, это:
format_float(char *buf, size_t buflen, PyFloatObject *v, int precision)
{
register char *cp;
char format[32];
int i;
/* Subroutine for float_repr and float_print.
We want float numbers to be recognizable as such,
i.e., they should contain a decimal point or an exponent.
However, %g may print the number as an integer;
in such cases, we append ".0" to the string. */
assert(PyFloat_Check(v));
PyOS_snprintf(format, 32, "%%.%ig", precision);
PyOS_ascii_formatd(buf, buflen, format, v->ob_fval);
cp = buf;
if (*cp == '-')
cp++;
for (; *cp != '\0'; cp++) {
/* Any non-digit means it not an integer;
this takes care of NAN and INF as well. */
if (!isdigit(Py_CHARMASK(*cp)))
break;
}
if (*cp == '\0') {
*cp++ = '.';
*cp++ = '0';
*cp++ = '\0';
return;
}
<some NaN/inf stuff>
}
Мы можем видеть, что
Таким образом, это сначала инициализирует некоторые переменные и проверки, что v
является корректным поплавком. Затем он подготавливает строку формата:
PyOS_snprintf(format, 32, "%%.%ig", precision);
Теперь PREC_REPR определяется в другом месте в floatobject.c
как 17, поэтому он вычисляет "%.17g"
. Теперь мы называем
PyOS_ascii_formatd(buf, buflen, format, v->ob_fval);
В конце туннеля мы видим PyOS_ascii_formatd
и обнаруживаем, что он использует snprintf
внутренне.
Ответ 3
из учебника python:
В версиях до Python 2.7 и Python 3.1 Python округлил это значение до 17 значащих цифр, предоставив ‘0.10000000000000001’
. В текущих версиях Python отображает значение, основанное на кратчайшей десятичной дроби, которая правильно вернется к истинному двоичному значению, в результате просто в ‘0.1’
.