Странное поведение с поплавками и преобразованием строк - программирование
Подтвердить что ты не робот

Странное поведение с поплавками и преобразованием строк

Я набрал это в оболочку 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, я также буду заинтересован в них.)

4b9b3361

Ответ 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’.