Mengapa nilai floating-point 4 * 0,1 terlihat bagus di Python 3 tetapi 3 * 0,1 tidak?

158

Saya tahu bahwa sebagian besar desimal tidak memiliki representasi floating point yang tepat ( Apakah matematika floating point rusak? ).

Tapi saya tidak melihat mengapa 4*0.1dicetak dengan baik 0.4, tetapi 3*0.1tidak, ketika kedua nilai sebenarnya memiliki representasi desimal jelek:

>>> 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')
Aivar
sumber
7
Karena beberapa angka dapat direpresentasikan dengan tepat, dan beberapa tidak.
Morgan Thrapp
58
@MorganThrapp: bukan itu. OP bertanya tentang pilihan format yang tampak sewenang-wenang. Baik 0,3 atau 0,4 tidak dapat diwakili tepat di titik mengambang biner.
Batsyeba
42
@BartoszKP: Setelah membaca dokumen beberapa kali, itu tidak menjelaskan mengapa Python adalah menampilkan 0.3000000000000000444089209850062616169452667236328125sebagai 0.30000000000000004dan 0.40000000000000002220446049250313080847263336181640625sebagai .4meskipun mereka tampaknya memiliki akurasi yang sama, dan dengan demikian tidak menjawab pertanyaan itu.
Mooing Duck
6
Lihat juga stackoverflow.com/questions/28935257/… - Saya agak kesal karena ditutup sebagai duplikat tetapi yang ini belum.
Random832
12
Dibuka kembali, tolong jangan tutup ini sebagai duplikat "matematika floating point rusak" .
Antti Haapala

Jawaban:

301

Jawaban sederhananya adalah karena 3*0.1 != 0.3kesalahan kuantisasi (pembulatan) (sedangkan 4*0.1 == 0.4karena mengalikan dengan kekuatan dua biasanya merupakan operasi "tepat").

Anda dapat menggunakan .hexmetode dalam Python untuk melihat representasi internal dari suatu bilangan (pada dasarnya, nilai titik mengambang biner yang tepat , daripada pendekatan basis-10). Ini dapat membantu menjelaskan apa yang terjadi di bawah tenda.

>>> (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 adalah 0x1.999999999999a kali 2 ^ -4. Huruf "a" pada akhirnya berarti angka 10 - dengan kata lain, 0,1 dalam titik apung biner sangat sedikit lebih besar dari nilai "tepat" 0,1 (karena 0x0,99 akhir dibulatkan menjadi 0x0,a). Ketika Anda mengalikan ini dengan 4, kekuatan dua, eksponen bergeser ke atas (dari 2 ^ -4 ke 2 ^ -2) tetapi jumlahnya dinyatakan tidak berubah, jadi 4*0.1 == 0.4.

Namun, ketika Anda mengalikan 3, perbedaan kecil antara 0x0.99 dan 0x0.a0 (0x0.07) memperbesar menjadi kesalahan 0x0.15, yang muncul sebagai kesalahan satu digit di posisi terakhir. Ini menyebabkan 0,1 * 3 menjadi sedikit lebih besar dari nilai bulat 0,3.

Float Python 3 reprdirancang untuk dapat trip-trippable , yaitu nilai yang ditampilkan harus benar-benar dapat dikonversi menjadi nilai aslinya. Oleh karena itu, ia tidak dapat menampilkan 0.3dan 0.1*3dengan cara yang persis sama, atau dua angka yang berbeda akan berakhir sama setelah tersandung. Akibatnya, reprmesin Python 3 memilih untuk menampilkan satu dengan kesalahan yang agak jelas.

nneonneo
sumber
25
Ini jawaban yang luar biasa komprehensif, terima kasih. (Khususnya, terima kasih telah menunjukkan .hex(); Saya tidak tahu itu ada.)
NPE
21
@supercat: Python mencoba menemukan string terpendek yang akan membulatkan ke nilai yang diinginkan , apa pun yang terjadi. Jelas nilai yang dievaluasi harus dalam 0,5ulp (atau akan membulatkan ke yang lain), tetapi mungkin membutuhkan lebih banyak digit dalam kasus-kasus yang ambigu. Kode ini sangat gnarly, tetapi jika Anda ingin mengintip: hg.python.org/cpython/file/03f2c8fc24ea/Python/dtoa.c#l2345
nneonneo
2
@supercat: Selalu string terpendek yang berada dalam 0,5 ulp. ( Ketat di dalam jika kita melihat pelampung dengan LSB ganjil; yaitu, string terpendek yang membuatnya bekerja dengan round-ties-to-even). Pengecualian untuk ini adalah bug, dan harus dilaporkan.
Mark Dickinson
7
@ Markarkh Tentunya mereka menggunakan sesuatu yang lain selain ekarena itu sudah hex digit. Mungkin puntuk kekuatan, bukan eksponen .
Bergi
11
@Bergi: Penggunaan pdalam konteks ini kembali (setidaknya) ke C99, dan juga muncul di IEEE 754 dan dalam berbagai bahasa lainnya (termasuk Jawa). Ketika float.hexdan float.fromhexdiimplementasikan (oleh saya :-), Python hanya menyalin apa yang pada saat itu ditetapkan praktiknya. Saya tidak tahu apakah niatnya adalah 'p' untuk "Kekuatan", tetapi sepertinya cara yang bagus untuk memikirkannya.
Mark Dickinson
75

repr(dan strdalam Python 3) akan mengeluarkan digit sebanyak yang diperlukan untuk membuat nilai tidak ambigu. Dalam hal ini hasil dari perkalian 3*0.1bukan nilai terdekat dengan 0,3 (0x1.3333333333333p-2 dalam hex), itu sebenarnya satu LSB lebih tinggi (0x1.333333333333334p-2) sehingga perlu lebih banyak digit untuk membedakannya dari 0,3.

Di sisi lain, perkalian 4*0.1 tidak mendapatkan nilai terdekat ke 0,4 (0x1.999999999999ap-2 dalam hex), sehingga tidak memerlukan digit tambahan.

Anda dapat memverifikasi ini dengan mudah:

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

Saya menggunakan notasi hex di atas karena bagus dan kompak dan menunjukkan perbedaan bit antara dua nilai. Anda dapat melakukannya sendiri menggunakan mis (3*0.1).hex(). Jika Anda lebih suka melihat mereka dalam semua kemuliaan desimal mereka, ini dia:

>>> 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')
Mark tebusan
sumber
2
(+1) Jawaban yang bagus, terima kasih. Apakah Anda pikir mungkin ada baiknya menggambarkan titik "bukan nilai terdekat" dengan memasukkan hasil dari 3*0.1 == 0.3dan 4*0.1 == 0.4?
NPE
@ NPE Saya harus melakukan itu langsung dari pintu gerbang, terima kasih atas sarannya.
Mark Ransom
Saya ingin tahu apakah perlu mencatat nilai desimal yang tepat dari "ganda" terdekat menjadi 0,1, 0,3, dan 0,4, karena banyak orang tidak dapat membaca hex floating-point.
supercat
@supercat Anda membuat poin yang bagus. Menempatkan ganda yang sangat besar ke dalam teks akan mengganggu, tetapi saya memikirkan cara untuk menambahkannya.
Mark Ransom
25

Inilah kesimpulan yang disederhanakan dari jawaban lain.

Jika Anda memeriksa pelampung pada baris perintah Python atau mencetaknya, ia akan melewati fungsi repryang menciptakan representasi stringnya.

Dimulai dengan versi 3.2, Python strdan reprmenggunakan skema pembulatan kompleks, yang lebih suka desimal yang tampak bagus jika mungkin, tetapi menggunakan lebih banyak digit jika diperlukan untuk menjamin pemetaan bijective (satu-ke-satu) antara float dan representasi string mereka.

Skema ini menjamin bahwa nilai repr(float(s))terlihat bagus untuk desimal sederhana, bahkan jika mereka tidak dapat direpresentasikan secara tepat sebagai float (mis. Kapan s = "0.1").

Pada saat yang sama itu menjamin bahwa float(repr(x)) == xberlaku untuk setiap kendaraanx

Aivar
sumber
2
Jawaban Anda akurat untuk versi Python> = 3.2, di mana strdan repridentik untuk pelampung. Untuk Python 2.7, reprmemiliki properti yang Anda identifikasi, tetapi strjauh lebih sederhana - itu hanya menghitung 12 digit signifikan dan menghasilkan string output berdasarkan pada mereka. Untuk Python <= 2.6, keduanya reprdan strdidasarkan pada jumlah tetap dari digit signifikan (17 untuk repr, 12 untuk str). (Dan tidak ada yang peduli tentang Python 3.0 atau Python 3.1 :-)
Mark Dickinson
Terima kasih @MarkDickinson! Saya memasukkan komentar Anda dalam jawabannya.
Aivar
2
Perhatikan bahwa pembulatan dari shell berasal reprsehingga perilaku Python 2.7 akan identik ...
Antti Haapala
5

Tidak benar-benar spesifik untuk implementasi Python tetapi harus diterapkan pada float untuk fungsi string desimal.

Angka floating point pada dasarnya adalah angka biner, tetapi dalam notasi ilmiah dengan batas tetap dari angka-angka penting.

Kebalikan dari angka apa pun yang memiliki faktor bilangan prima yang tidak dibagi dengan basis akan selalu menghasilkan representasi titik titik berulang. Misalnya 1/7 memiliki faktor prima, 7, yang tidak dibagi dengan 10, dan karenanya memiliki representasi desimal berulang, dan hal yang sama berlaku untuk 1/10 dengan faktor prima 2 dan 5, yang terakhir tidak dibagikan dengan 2 ; ini berarti bahwa 0,1 tidak dapat secara tepat diwakili oleh jumlah bit yang terbatas setelah titik titik.

Karena 0,1 tidak memiliki representasi yang tepat, fungsi yang mengubah perkiraan menjadi string titik desimal biasanya akan mencoba memperkirakan nilai tertentu sehingga mereka tidak mendapatkan hasil yang tidak intuitif seperti 0,1000000000004121.

Karena floating point dalam notasi ilmiah, setiap perkalian dengan kekuatan basis hanya mempengaruhi bagian eksponen dari angka tersebut. Misalnya 1.231e + 2 * 100 = 1.231e + 4 untuk notasi desimal, dan juga, 1.00101010e11 * 100 = 1.00101010e101 dalam notasi biner. Jika saya kalikan dengan non-power dari pangkalan, angka yang signifikan juga akan terpengaruh. Misalnya 1.2e1 * 3 = 3.6e1

Bergantung pada algoritma yang digunakan, mungkin mencoba menebak desimal umum berdasarkan angka signifikan saja. Baik 0,1 dan 0,4 memiliki angka signifikan yang sama dalam biner, karena mengapung mereka pada dasarnya pemotongan (8/5) (2 ^ -4) dan (8/5) (2 ^ -6). Jika algoritme mengidentifikasi pola sigfig 8/5 sebagai desimal 1.6, maka ia akan bekerja pada 0,1, 0,2, 0,4, 0,8, dll. Mungkin juga memiliki pola sigfig ajaib untuk kombinasi lain, seperti float 3 dibagi dengan float 10 dan pola ajaib lainnya secara statistik kemungkinan akan dibentuk oleh pembagian oleh 10.

Dalam kasus 3 * 0,1, beberapa angka penting terakhir kemungkinan akan berbeda dari membagi float 3 dengan float 10, menyebabkan algoritma gagal mengenali angka ajaib untuk konstanta 0.3 tergantung pada toleransinya terhadap kehilangan presisi.

Edit: https://docs.python.org/3.1/tutorial/floatingpoint.html

Menariknya, ada banyak angka desimal berbeda yang memiliki fraksi biner perkiraan terdekat yang sama. Misalnya, angka 0,1 dan 0,10000000000000001 dan 0,100000000000000005555511151231257827021181583404541015625 semuanya didekati oleh 3602879701896397/2 ** 55. Karena semua nilai desimal ini memiliki pendekatan yang sama, maka salah satu dari mereka dapat ditampilkan (tetap tetap). ) == x.

Tidak ada toleransi untuk kehilangan presisi, jika float x (0,3) tidak persis sama dengan float y (0,1 * 3), maka repr (x) tidak persis sama dengan repr (y).

AkariAkaori
sumber
4
Ini tidak benar-benar menambah banyak jawaban yang ada.
Antti Haapala
1
"Bergantung pada algoritma yang digunakan, itu mungkin mencoba untuk menebak desimal umum berdasarkan angka signifikan saja." <- Ini sepertinya spekulasi murni. Jawaban lain telah menjelaskan apa yang sebenarnya dilakukan Python .
Mark Dickinson