Чому в Python 3 добре виглядає значення з плаваючою комою 4 * 0,1, але 3 * 0,1 не відповідає?


158

Я знаю, що більшість десяткових знаків не мають точного подання з плаваючою комою ( Чи порушена математика з плаваючою комою? ).

Але я не бачу, чому так 4*0.1добре друкується 0.4, але 3*0.1ні, коли обидва значення насправді мають некрасиві десяткові зображення:

>>> 3*0.1
0.30000000000000004
>>> 4*0.1
0.4
>>> from decimal import Decimal
>>> Decimal(3*0.1)
Decimal('0.3000000000000000444089209850062616169452667236328125')
>>> Decimal(4*0.1)
Decimal('0.40000000000000002220446049250313080847263336181640625')

7
Тому що деякі числа можуть бути представлені точно, а деякі не можуть.
Морган Трапп

58
@MorganThrapp: ні, це не так. ОП запитує про досить формальний вибір форматування. Ні 0,3, ні 0,4 не можуть бути представлені точно у двійковій плаваючій точці.
Вірсавія

42
@BartoszKP: Прочитавши документ кілька разів, це не пояснює, чому Python відображається 0.3000000000000000444089209850062616169452667236328125як 0.30000000000000004і 0.40000000000000002220446049250313080847263336181640625як, .4хоча вони, схоже, мають однакову точність, і тому не відповідають на питання.
Mooing Duck

6
Дивіться також stackoverflow.com/questions/28935257/… - я дещо роздратований тим, що він закрився як дублікат, але цей ще ні.
Випадково832

12
Повторно відкрийте, будь ласка, не закривайте це, оскільки дублікат "математика з плаваючою комою порушена" .
Антті Хаапала

Відповіді:


301

Проста відповідь полягає в тому, що 3*0.1 != 0.3через помилку квантування (округлення) (тоді 4*0.1 == 0.4як множення на потужність двох зазвичай є "точною" операцією).

Ви можете використовувати .hexметод в Python для перегляду внутрішнього подання числа (в основному, точне двійкове значення з плаваючою комою, а не наближення бази-10). Це може допомогти пояснити, що відбувається під кришкою.

>>> (0.1).hex()
'0x1.999999999999ap-4'
>>> (0.3).hex()
'0x1.3333333333333p-2'
>>> (0.1*3).hex()
'0x1.3333333333334p-2'
>>> (0.4).hex()
'0x1.999999999999ap-2'
>>> (0.1*4).hex()
'0x1.999999999999ap-2'

0,1 дорівнює 0x1,999999999999a разів 2 ^ -4. "А" в кінці означає цифру 10 - іншими словами, 0,1 у двійковій плаваючій точці дуже трохи більше, ніж "точне" значення 0,1 (оскільки кінцеве значення 0x0,99 округляється до 0x0.a). Коли ви помножите це на 4, потужність дві, показник зміщується вгору (від 2 ^ -4 до 2 ^ -2), але число інакше не змінюється, так 4*0.1 == 0.4.

Однак, якщо ви помножите на 3, маленька крихітна різниця між 0x0,99 та 0x0.a0 (0x0,07) збільшується до помилки 0x0,15, яка відображається як одноцифрова помилка в останньому положенні. Це призводить до того, що 0,1 * 3 буде дещо більшим, ніж округлене значення 0,3.

Поплавок Python 3 reprрозроблений таким чином, що він може бути зворотним , тобто показане значення повинно бути точно конвертоване в початкове значення. Тому він не може відображати 0.3і 0.1*3точно таким же чином, або дві різні цифри будуть в кінцевому підсумку те ж саме після того, як кругообіг. Отже, reprдвигун Python 3 вирішує відображати його з невеликою явною помилкою.


25
Це надзвичайно вичерпна відповідь, дякую. (Зокрема, дякую за показ .hex(); я не знав, що воно існує.)
NPE,

21
@supercat: Python намагається знайти найкоротший рядок, який би округлив до потрібного значення , що б там не сталося. Очевидно, що оцінене значення повинно бути в межах 0,5ulp (або воно округлене до чогось іншого), але воно може зажадати більше цифр у неоднозначних випадках. Код дуже гострий, але якщо ви хочете заглянути: hg.python.org/cpython/file/03f2c8fc24ea/Python/dtoa.c#l2345
nneonneo

2
@supercat: Завжди найкоротший рядок, що знаходиться в межах 0,5 ulp. ( Суворо, якщо ми дивимося на поплавок з непарним LSB; тобто, найкоротший рядок, який змушує його працювати з круглими зв'язками до парних). Будь-які винятки з цього питання є помилкою, і про них слід повідомити.
Марк Дікінсон

7
@MarkRansom Звичайно, вони використовували щось інше, eтому що це вже шістнадцятковий розряд. Можливо, pдля влади замість показника .
Бергі

11
@Bergi: Використання pв цьому контексті сягає (принаймні) на C99, а також з'являється в IEEE 754 та в інших інших мовах (включаючи Java). Коли float.hexі float.fromhexбули реалізовані (мені :-), Python просто копіював те, що було на той час встановленою практикою. Я не знаю, чи був цей намір "p" для "Power", але це здається приємним способом подумати над цим.
Марк Дікінсон

75

reprstrв Python 3) викладе стільки цифр, скільки потрібно, щоб зробити значення однозначним. У цьому випадку результат множення 3*0.1не є найближчим до значення 0,3 (0x1,3333333333333p-2 у шістнадцятковій формі), це насправді на один LSB вище (0x1,3333333333334p-2), тому для розрізнення його потрібно більше цифр.

З іншого боку, множення 4*0.1 робить отримати найбільш близьке значення 0,4 (0x1.999999999999ap-2 в шістнадцятковій формі ), так що не потрібно ніяких додаткових цифр.

Ви можете перевірити це досить легко:

>>> 3*0.1 == 0.3
False
>>> 4*0.1 == 0.4
True

Я використовував шістнадцяткові позначення, тому що це приємно і компактно і показує різницю між двома значеннями. Ви можете зробити це самостійно, використовуючи напр (3*0.1).hex(). Якщо ви хочете побачити їх у всій їх десятковій красі, ось вам:

>>> Decimal(3*0.1)
Decimal('0.3000000000000000444089209850062616169452667236328125')
>>> Decimal(0.3)
Decimal('0.299999999999999988897769753748434595763683319091796875')
>>> Decimal(4*0.1)
Decimal('0.40000000000000002220446049250313080847263336181640625')
>>> Decimal(0.4)
Decimal('0.40000000000000002220446049250313080847263336181640625')

2
(+1) Приємна відповідь, дякую. Як ви вважаєте, можливо, варто проілюструвати точку "не найближчого значення", включивши результат 3*0.1 == 0.3та 4*0.1 == 0.4?
NPE

@NPE Я повинен був зробити це прямо за воротами, дякую за пропозицію.
Марк Рансом

Цікаво, чи варто було б відзначити точні десяткові значення найближчих «подвоєнь» до 0,1, 0,3 та 0,4, оскільки багато людей не можуть прочитати шестигранну плаваючу крапку.
supercat

@supercat ви добре задумаєте. Введення цих супервеликих пар у текст було б відволікаючим, але я придумав спосіб їх додати.
Марк Рансом

25

Ось спрощений висновок з інших відповідей.

Якщо ви перевіряєте поплавок у командному рядку Python або друкуєте його, він проходить через функцію, reprяка створює його рядкове подання.

Починаючи з версії 3.2, Python strі reprвикористовує складну схему округлення, яка, якщо можливо, віддає перевагу приємним децималам, але використовує більше цифр, де це необхідно, щоб гарантувати бієктивне (однозначне) відображення між поплавками та їх рядковими зображеннями.

Ця схема гарантує, що значення repr(float(s))виглядає добре для простих десяткових знаків, навіть якщо вони не можуть бути представлені точно як поплавці (наприклад, коли s = "0.1").

У той же час він гарантує, що float(repr(x)) == xтримається за кожен поплавокx


2
Ваша відповідь точна для версій Python> = 3.2, де strі reprоднакові для поплавків. Для Python 2.7 reprмає властивості, які ви ідентифікуєте, але strнабагато простіше - він просто обчислює 12 значущих цифр і виробляє вихідний рядок на основі цих. Для Python <= 2.6 обидва reprі strбазуються на фіксованій кількості значущих цифр (17 для repr, 12 для str). (І ніхто не піклується про Python 3.0 або Python 3.1 :-)
Марк Дікінсон

Дякую @MarkDickinson! Я включив ваш коментар у відповідь.
Айвар

2
Зауважте, що округлення з оболонки походить від reprтаким чином поведінка Python 2.7 була б ідентичною ...
Antti Haapala

5

Насправді не характерно для реалізації Python, але має застосовуватися до будь-яких функцій поплавця до десяткових рядків.

Число з плаваючою комою є по суті двійковим числом, але в наукових позначеннях із фіксованою межею значущих цифр.

Зворотне будь-яке число, яке має простий коефіцієнт числа, яке не поділяється з базою, завжди призведе до повторного подання точкових точок. Наприклад, 1/7 має простий коефіцієнт 7, який не поділяється на 10, а тому має повторне десяткове подання, і те саме стосується 1/10 з простими множниками 2 і 5, останній не ділиться з 2 ; це означає, що 0,1 не може бути точно представлений кінцевою кількістю бітів після крапки.

Оскільки 0,1 не має точного подання, функція, яка перетворює наближення до рядка з десятковою точкою, зазвичай намагається наблизити певні значення, щоб вони не отримували інтуїтивні результати, наприклад, 0,1000000000004121.

Оскільки плаваюча точка знаходиться в науковій нотації, будь-яке множення на силу основи впливає лише на експонентну частину числа. Наприклад, 1,231e + 2 * 100 = 1,231e + 4 для десяткової нотації, а також 1,00101010e11 * 100 = 1,00101010e101 у двійковій нотації. Якщо я помножую на не-потужність бази, це також вплине на значні цифри. Наприклад, 1,2e1 * 3 = 3,6e1

Залежно від використовуваного алгоритму, він може спробувати відгадати загальні десяткові знаки лише на основі значущих цифр. І 0,1, і 0,4 мають однакові значні цифри у двійкових, оскільки їх поплавці по суті є усіченнями (8/5) (2 ^ -4) та (8/5) (2 ^ -6) відповідно. Якщо алгоритм ідентифікує шаблон сигфіг 8/5 як десятковий 1.6, він буде працювати на 0,1, 0,2, 0,4, 0,8 і т.д. Він може також мати магічні шаблони сигфіг для інших комбінацій, наприклад, поплавок 3, поділений на поплавок 10 та інші магічні візерунки, статистично ймовірно, будуть сформовані діленням на 10.

У випадку 3 * 0,1 останні кілька значущих цифр, ймовірно, будуть відрізнятися від ділення поплавця 3 на поплавок 10, внаслідок чого алгоритм не може розпізнати магічне число для константи 0,3 залежно від його допуску до втрати точності.

Редагувати: https://docs.python.org/3.1/tutorial/floatingpoint.html

Цікаво, що існує багато різних десяткових чисел, які поділяють однаковий найближчий приблизний двійковий дріб. Наприклад, числа 0,1 і 0,10000000000000001 та 0,1000000000000000055511151231257827021181583404541015625 всі наближені на 3602879701896397/2 ** 55. Оскільки всі ці десяткові значення мають однакове наближення, будь-яке з них може бути відображене при збереженні інваріантного рівня (repr (x)) ) == х.

Немає допуску до втрати точності, якщо float x (0,3) не точно дорівнює float y (0,1 * 3), то repr (x) точно не дорівнює repr (y).


4
Це насправді не додає багато відповідей до існуючих.
Антті Хаапала

1
"Залежно від використовуваного алгоритму, він може намагатися відгадати загальні десяткові знаки лише на основі значущих цифр." <- Це здається чистою спекуляцією. Інші відповіді описали, що насправді робить Python .
Марк Дікінсон,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.