Tại sao giá trị dấu phẩy động 4 * 0.1 trông đẹp trong Python 3 nhưng 3 * 0.1 thì không?


158

Tôi biết rằng hầu hết các số thập phân không có biểu diễn dấu phẩy động chính xác ( Toán học dấu phẩy động có bị hỏng không? ).

Nhưng tôi không hiểu tại sao 4*0.1được in độc đáo như 0.4, nhưng 3*0.1không, khi cả hai giá trị thực sự có biểu diễn thập phân xấu xí:

>>> 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
Bởi vì một số con số có thể được biểu diễn chính xác, và một số không thể.
Morgan Thrapp

58
@MorganThrapp: không, không. OP đang hỏi về sự lựa chọn định dạng trông khá độc đoán. Cả 0,3 và 0,4 có thể được biểu diễn chính xác trong dấu phẩy động nhị phân.
Bathsheba

42
@BartoszKP: Sau khi đọc tài liệu nhiều lần, nó không giải thích tại sao Python được hiển thị 0.3000000000000000444089209850062616169452667236328125như 0.300000000000000040.40000000000000002220446049250313080847263336181640625như .4mặc dù họ dường như có tính chính xác tương tự, và do đó không trả lời câu hỏi.
Vịt Mooing

6
Xem thêm stackoverflow.com/questions/28935257/ - Tôi hơi khó chịu khi nó bị đóng như một bản sao nhưng cái này thì không.
Random832

12
Đã mở lại, vui lòng không đóng cái này vì bản sao của "toán học dấu phẩy động bị hỏng" .
Antti Haapala

Câu trả lời:


301

Câu trả lời đơn giản là 3*0.1 != 0.3do lỗi lượng tử hóa (làm tròn) (trong khi 4*0.1 == 0.4vì nhân với lũy thừa hai thường là một thao tác "chính xác").

Bạn có thể sử dụng .hexphương thức trong Python để xem biểu diễn bên trong của một số (về cơ bản, giá trị dấu phẩy động chính xác nhị phân, thay vì xấp xỉ cơ số 10). Điều này có thể giúp giải thích những gì đang diễn ra dưới mui xe.

>>> (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 là 0x1.999999999999a lần 2 ^ -4. Chữ "a" ở cuối có nghĩa là chữ số 10 - nói cách khác, 0,1 trong dấu phẩy động nhị phân lớn hơn một chút so với giá trị "chính xác" là 0,1 (vì 0x0,99 cuối cùng được làm tròn lên đến 0x0.a). Khi bạn nhân số này với 4, lũy thừa của hai, số mũ sẽ tăng lên (từ 2 ^ -4 đến 2 ^ -2) nhưng số khác thì không thay đổi, vì vậy 4*0.1 == 0.4.

Tuy nhiên, khi bạn nhân với 3, sự khác biệt nhỏ bé giữa 0x0,99 và 0x0.a0 (0x0,07) sẽ phóng đại thành lỗi 0x0.15, xuất hiện dưới dạng lỗi một chữ số ở vị trí cuối cùng. Điều này làm cho 0,1 * 3 lớn hơn một chút so với giá trị làm tròn là 0,3.

Phao của Python 3 reprđược thiết kế để có thể cắt tròn , nghĩa là giá trị được hiển thị phải được chuyển đổi chính xác thành giá trị ban đầu. Do đó, nó không thể hiển thị 0.30.1*3chính xác theo cùng một cách, hoặc hai số khác nhau sẽ kết thúc giống nhau sau khi vấp vòng. Do đó, reprcông cụ của Python 3 chọn hiển thị một lỗi với một lỗi rõ ràng.


25
Đây là một câu trả lời toàn diện đáng kinh ngạc, cảm ơn bạn. (Đặc biệt, cảm ơn vì đã hiển thị .hex(); tôi không biết nó tồn tại.)
NPE

21
@supercat: Python cố gắng tìm chuỗi ngắn nhất sẽ làm tròn đến giá trị mong muốn , bất kể điều gì xảy ra. Rõ ràng giá trị được đánh giá phải nằm trong 0,5ulp (hoặc nó sẽ làm tròn sang thứ khác), nhưng nó có thể yêu cầu nhiều chữ số hơn trong các trường hợp mơ hồ. Mã này rất hầm hố
nneonneo

2
@supercat: Luôn là chuỗi ngắn nhất trong vòng 0,5 ulp. ( Nghiêm túc trong nếu chúng ta đang nhìn vào một float với LSB kỳ lạ; tức là chuỗi ngắn nhất làm cho nó hoạt động với các mối quan hệ tròn đến chẵn). Bất kỳ trường hợp ngoại lệ nào là lỗi này và cần được báo cáo.
Đánh dấu Dickinson

7
@MarkRansom Chắc chắn họ đã sử dụng một cái gì đó khác hơn là evì đó đã là một chữ số hex. Có thể pcho quyền lực thay vì số mũ .
Bergi

11
@Bergi: Việc sử dụng ptrong ngữ cảnh này quay trở lại (ít nhất là) đối với C99 và cũng xuất hiện trong IEEE 754 và trong các ngôn ngữ khác (bao gồm cả Java). Khi float.hexfloat.fromhexđược thực hiện (bởi tôi :-), Python chỉ sao chép những gì sau đó được thiết lập thực tiễn. Tôi không biết liệu ý định đó có phải là "Sức mạnh" hay không, nhưng có vẻ như đó là một cách hay để suy nghĩ về nó.
Đánh dấu Dickinson

75

repr(và strtrong Python 3) sẽ đưa ra nhiều chữ số theo yêu cầu để làm cho giá trị không rõ ràng. Trong trường hợp này, kết quả của phép nhân 3*0.1không phải là giá trị gần nhất với 0,3 (0x1.3333333333333p-2 trong hex), thực tế nó là một LSB cao hơn (0x1.3333333333334p-2) vì vậy nó cần nhiều chữ số hơn để phân biệt với 0,3.

Mặt khác, các nhân 4*0.1 không nhận được giá trị gần 0,4 (0x1.999999999999ap-2 trong hex), vì vậy nó không cần bất kỳ chữ số bổ sung.

Bạn có thể xác minh điều này khá dễ dàng:

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

Tôi đã sử dụng ký hiệu hex ở trên vì nó đẹp và nhỏ gọn và hiển thị sự khác biệt bit giữa hai giá trị. Bạn có thể tự làm điều này bằng cách sử dụng ví dụ (3*0.1).hex(). Nếu bạn muốn nhìn thấy chúng trong tất cả vinh quang thập phân của chúng, thì ở đây bạn đi:

>>> 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) Câu trả lời hay, cảm ơn. Bạn có nghĩ rằng nó có thể có giá trị minh họa điểm "không phải giá trị gần nhất" bằng cách bao gồm kết quả của 3*0.1 == 0.34*0.1 == 0.4?
NPE

@NPE Tôi nên làm điều đó ngay khi ra khỏi cổng, cảm ơn vì lời đề nghị.
Đánh dấu tiền chuộc

Tôi tự hỏi liệu có đáng để lưu ý các giá trị thập phân chính xác của "nhân đôi" gần nhất thành 0,1, 0,3 và 0,4 hay không, vì nhiều người không thể đọc được dấu phẩy động.
supercat

@supercat bạn làm điểm tốt. Đặt những nhân đôi siêu lớn đó vào văn bản sẽ gây mất tập trung, nhưng tôi nghĩ ra một cách để thêm chúng.
Đánh dấu tiền chuộc

25

Đây là một kết luận đơn giản từ các câu trả lời khác.

Nếu bạn kiểm tra một dấu phẩy trên dòng lệnh của Python hoặc in nó, nó sẽ đi qua hàm reprtạo ra biểu diễn chuỗi của nó.

Bắt đầu với phiên bản 3.2, Python strreprsử dụng sơ đồ làm tròn phức tạp, ưu tiên các số thập phân trông đẹp mắt nếu có thể, nhưng sử dụng nhiều chữ số hơn khi cần để đảm bảo ánh xạ phỏng đoán (một-một) giữa các float và biểu diễn chuỗi của chúng.

Lược đồ này đảm bảo rằng giá trị của repr(float(s))ngoại hình đẹp đối với các số thập phân đơn giản, ngay cả khi chúng không thể được biểu diễn chính xác dưới dạng nổi (ví dụ: khi nào s = "0.1").

Đồng thời nó đảm bảo float(repr(x)) == xgiữ cho mọi floatx


2
Câu trả lời của bạn là chính xác cho các phiên bản Python> = 3.2, ở đâu strreprgiống hệt nhau cho số float. Đối với Python 2.7, reprcó các thuộc tính bạn xác định, nhưng strđơn giản hơn nhiều - nó chỉ đơn giản tính 12 chữ số có nghĩa và tạo ra một chuỗi đầu ra dựa trên các thuộc tính đó. Đối với Python <= 2.6, cả hai reprstrđều dựa trên một số chữ số có nghĩa cố định (17 cho repr, 12 cho str). (Và không ai quan tâm đến Python 3.0 hoặc Python 3.1 :-)
Mark Dickinson

Cảm ơn @MarkDickinson! Tôi bao gồm nhận xét của bạn trong câu trả lời.
Aivar

2
Lưu ý rằng làm tròn từ vỏ đến từ reprhành vi Python 2.7 sẽ giống hệt nhau ...
Antti Haapala

5

Không thực sự cụ thể đối với việc triển khai của Python nhưng nên áp dụng cho bất kỳ hàm float nào trong chuỗi thập phân.

Số dấu phẩy động thực chất là số nhị phân, nhưng theo ký hiệu khoa học với giới hạn cố định của các số liệu có ý nghĩa.

Nghịch đảo của bất kỳ số nào có yếu tố số nguyên tố không được chia sẻ với cơ sở sẽ luôn dẫn đến biểu diễn điểm chấm định kỳ. Ví dụ 1/7 có thừa số nguyên tố 7, không được chia sẻ với 10 và do đó có biểu diễn thập phân định kỳ và điều này cũng đúng với 1/10 với các thừa số nguyên tố 2 và 5, yếu tố sau không được chia sẻ với 2 ; điều này có nghĩa là 0,1 không thể được biểu diễn chính xác bằng số bit hữu hạn sau điểm chấm.

Vì 0,1 không có biểu diễn chính xác, nên một hàm chuyển đổi xấp xỉ thành chuỗi dấu thập phân thường sẽ cố gắng xấp xỉ các giá trị nhất định để chúng không nhận được kết quả không trực quan như 0.1000000000004121.

Vì dấu phẩy động nằm trong ký hiệu khoa học, bất kỳ phép nhân nào với lũy thừa của cơ sở chỉ ảnh hưởng đến phần số mũ của số. Ví dụ: 1.231e + 2 * 100 = 1.231e + 4 cho ký hiệu thập phân và tương tự, 1.00101010e11 * 100 = 1.00101010e101 trong ký hiệu nhị phân. Nếu tôi nhân với một sức mạnh không phải của cơ sở, các chữ số có nghĩa cũng sẽ bị ảnh hưởng. Ví dụ: 1,2e1 * 3 = 3,6e1

Tùy thuộc vào thuật toán được sử dụng, nó có thể cố gắng đoán các số thập phân phổ biến chỉ dựa trên các số liệu quan trọng. Cả 0,1 và 0,4 có cùng một số liệu có ý nghĩa trong nhị phân, bởi vì các phao của chúng về cơ bản là cắt ngắn (8/5) (2 ^ -4) và (8/5) (2 ^ -6). Nếu thuật toán xác định mẫu sigfig 8/5 là số thập phân 1.6, thì nó sẽ hoạt động trên 0,1, 0,2, 0,4, 0,8, v.v. Nó cũng có thể có các mẫu sigfig ma thuật cho các kết hợp khác, chẳng hạn như float 3 chia cho float 10 và các mẫu ma thuật khác có khả năng thống kê được hình thành bằng cách chia cho 10.

Trong trường hợp 3 * 0,1, một vài con số quan trọng cuối cùng có thể sẽ khác với việc chia phao 3 cho phao 10, khiến thuật toán không thể nhận ra số ma thuật cho hằng số 0,3 tùy thuộc vào khả năng chịu mất độ chính xác của nó.

Chỉnh sửa: https://docs.python.org/3.1/tutorial/floatingpoint.html

Thật thú vị, có nhiều số thập phân khác nhau có chung phân số nhị phân gần đúng nhất. Ví dụ: các số 0.1 và 0.10000000000000001 và 0.1000000000000000055511151231257827021181583404541015625 đều được xấp xỉ bằng 3602879701896394/2 ** 55. Vì tất cả các giá trị thập phân này đều có cùng một giá trị gần đúng. ) == x.

Không có dung sai cho độ mất độ chính xác, nếu float x (0.3) không chính xác bằng float y (0.1 * 3), thì repr (x) không chính xác bằng repr (y).


4
Điều này không thực sự thêm nhiều vào các câu trả lời hiện có.
Antti Haapala

1
"Tùy thuộc vào thuật toán được sử dụng, nó có thể cố gắng đoán các số thập phân phổ biến chỉ dựa trên các số liệu quan trọng." <- Điều này có vẻ như đầu cơ thuần túy. Các câu trả lời khác đã mô tả những gì Python thực sự làm.
Đánh dấu Dickinson
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.